<?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: Karina Pérez</title>
    <description>The latest articles on DEV Community by Karina Pérez (@karinapp).</description>
    <link>https://dev.to/karinapp</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%2F4192603%2Fdb85b87f-166f-4c7e-9885-bca2cf95dca0.jpg</url>
      <title>DEV Community: Karina Pérez</title>
      <link>https://dev.to/karinapp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karinapp"/>
    <language>en</language>
    <item>
      <title>Un servicio en el centro: páginas de Transit Gateway, grupos de seguridad y API Gateway</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:55:13 +0000</pubDate>
      <link>https://dev.to/karinapp/un-servicio-en-el-centro-paginas-de-transit-gateway-grupos-de-seguridad-y-api-gateway-23a7</link>
      <guid>https://dev.to/karinapp/un-servicio-en-el-centro-paginas-de-transit-gateway-grupos-de-seguridad-y-api-gateway-23a7</guid>
      <description>&lt;p&gt;Cómo VPC Atlas 0.9.0 dibuja un servicio por página, con sus tablas y hallazgos, y lo que esas páginas todavía no cuentan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las preguntas llegaban de una en una
&lt;/h2&gt;

&lt;p&gt;En una revisión reciente de la red, cada pregunta trataba de un solo servicio. ¿A dónde va esta ruta del Transit Gateway? ¿Qué security group está abierto al mundo y cuál no tiene nada asociado? ¿Dónde acaba cada ruta de esta API? VPC Atlas contesta con la página de región, y la página de región lo dibuja todo. La respuesta a cada una de esas preguntas está ahí, pero desparramada por toda la página.&lt;/p&gt;

&lt;p&gt;La versión 0.9.0, publicada el 20 de octubre de 2026, añade &lt;code&gt;--focus&lt;/code&gt;: una página por servicio, unas tablas junto al inventario y tres hallazgos que la herramienta marca en esas páginas y tablas. Hay además un cambio pequeño que no tiene que ver con focus: las páginas de VPC ahora muestran en el subtítulo las IP libres de cada subnet, como en &lt;code&gt;10.0.1.0/24 | 251 free IPs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  La opción
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas        &lt;span class="c"&gt;# o: curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1 &lt;span class="nt"&gt;--focus&lt;/span&gt; tgw,sg,apigw &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--focus VIEW&lt;/code&gt; funciona en &lt;code&gt;discover&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;diagram&lt;/code&gt; y &lt;code&gt;serve&lt;/code&gt;. Se puede repetir o separar las vistas con comas. Hay tres, y se aceptan alias como &lt;code&gt;transit-gateway&lt;/code&gt; y &lt;code&gt;api-gateway&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tgw&lt;/code&gt; escribe una página por Transit Gateway, tuyo o compartido contigo, con sus tablas de rutas, sus rutas y sus attachments (uno compartido se dibuja con lo que tus credenciales pueden leer, ver los límites más abajo). &lt;code&gt;sg&lt;/code&gt; escribe una página por VPC, con sus security groups, sus reglas y lo que tienen asociado. &lt;code&gt;apigw&lt;/code&gt; escribe una página por región, con sus API, endpoints, dominios y destinos de integración. Las páginas salen en draw.io y Lucid como &lt;code&gt;diagrams/vpcatlas-focus-&amp;lt;view&amp;gt;-&amp;lt;id&amp;gt;.&amp;lt;ext&amp;gt;&lt;/code&gt;, con el mismo motor de layout que las demás (&lt;a href="https://vpcatlas.com/docs/outputs/diagrams/#focus-pages" rel="noopener noreferrer"&gt;páginas focus&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Con &lt;code&gt;--focus&lt;/code&gt;, &lt;code&gt;--level&lt;/code&gt; genera por defecto solo las páginas focus; si le pasas &lt;code&gt;--level account&lt;/code&gt;, &lt;code&gt;vpc&lt;/code&gt; o &lt;code&gt;all&lt;/code&gt;, se añaden esas páginas. Sin &lt;code&gt;--focus&lt;/code&gt; no cambia nada. &lt;code&gt;--detail&lt;/code&gt; no toca una página focus, y &lt;code&gt;--tag&lt;/code&gt; reduce el grafo antes, así que un recurso que &lt;code&gt;--tag&lt;/code&gt; deja fuera tampoco sale en una página focus.&lt;/p&gt;

&lt;p&gt;En &lt;code&gt;discover&lt;/code&gt; y &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;--focus&lt;/code&gt; escribe además seis tablas junto al inventario: &lt;code&gt;tgw-attachments&lt;/code&gt;, &lt;code&gt;tgw-routes&lt;/code&gt;, &lt;code&gt;sg-rules&lt;/code&gt;, &lt;code&gt;sg-attachments&lt;/code&gt;, &lt;code&gt;apigw-apis&lt;/code&gt; y &lt;code&gt;apigw-integrations&lt;/code&gt;. Cada una es un fichero &lt;code&gt;vpcatlas-focus-&amp;lt;table&amp;gt;&lt;/code&gt; en todos los formatos de inventario que hayas elegido (json, md, html, csv). El JSON lleva el id de esquema &lt;code&gt;vpcatlas-focus/1&lt;/code&gt;, y las filas van ordenadas para que el mismo grafo dé siempre los mismos bytes. Las columnas están en la sección de &lt;a href="https://vpcatlas.com/docs/outputs/inventory/#focus-tables" rel="noopener noreferrer"&gt;tablas focus&lt;/a&gt; de la documentación.&lt;/p&gt;

&lt;p&gt;Tres hallazgos aparecen en las páginas y en las tablas. Los textos de las tarjetas los imprime la herramienta en inglés, tal cual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;open_ingress:&amp;lt;proto&amp;gt;/&amp;lt;ports&amp;gt;&lt;/code&gt;, en un security group abierto al mundo (orígenes 0.0.0.0/0 o ::/0) en un puerto que no sea el 80 ni el 443. La tarjeta dice &lt;code&gt;! open to the world: tcp 22&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unused&lt;/code&gt;, en un grupo que nada usa y que ninguna regla nombra. La tarjeta dice &lt;code&gt;! not attached&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default_endpoint_open&lt;/code&gt;, en una API con dominio propio cuyo endpoint &lt;code&gt;execute-api&lt;/code&gt; por defecto sigue activo. La tarjeta dice &lt;code&gt;! default endpoint open&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hay una cuarta marca, en la página del Transit Gateway. Un peering que nadie ha aceptado aparece como &lt;code&gt;pending_acceptance&lt;/code&gt; en la tabla de attachments, y su estado va entre paréntesis en la línea que lo une a su caja.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unused&lt;/code&gt; no se marca si un servicio que lleva security groups quedó fuera del escaneo con &lt;code&gt;--exclude-services&lt;/code&gt;, o no se pudo leer. Un grupo que parece sin uso solo porque el escaneo nunca miró Lambda ni ECS sería una falsa alarma, y prefiero que la herramienta calle a que marque algo equivocado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que medí
&lt;/h2&gt;

&lt;p&gt;Las cifras salen de la cuenta simulada del repositorio (&lt;code&gt;demo/fake_aws.py&lt;/code&gt;, moto 5.2.4, la biblioteca de Python que imita AWS, detrás de un shim pequeño), en &lt;code&gt;eu-west-1&lt;/code&gt;, con la 0.9.0 ya publicada. Amplié la cuenta para esta versión, de modo que las tres vistas tuvieran algo que enseñar. Ahora tiene una instancia bastion en una subnet pública con un grupo &lt;code&gt;bastion&lt;/code&gt; (SSH abierto al mundo), un grupo &lt;code&gt;legacy-app&lt;/code&gt; con RDP abierto al mundo y nada asociado, una API HTTP &lt;code&gt;prod-http&lt;/code&gt; y una API REST &lt;code&gt;partner-rest&lt;/code&gt;, una VPN &lt;code&gt;dc-vpn&lt;/code&gt; conectada al Transit Gateway &lt;code&gt;hub&lt;/code&gt;, un peering &lt;code&gt;hub-frankfurt&lt;/code&gt;, que nadie ha aceptado, con un Transit Gateway de &lt;code&gt;eu-central-1&lt;/code&gt; en otra cuenta, y una segunda tabla de rutas, &lt;code&gt;hub-partners&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover -r eu-west-1 --focus tgw,sg,apigw --output-dir vpcatlas-out&lt;/code&gt; encuentra 113 recursos y 0 incidencias (ninguna llamada denegada ni fallida), y escribe 5 páginas focus: 1 del Transit Gateway, 3 de security groups (la cuenta tiene 3 VPC) y 1 con las API de la región. Las seis tablas tienen 7 filas en &lt;code&gt;tgw-routes&lt;/code&gt;, 4 en &lt;code&gt;tgw-attachments&lt;/code&gt;, 20 en &lt;code&gt;sg-rules&lt;/code&gt;, 31 en &lt;code&gt;sg-attachments&lt;/code&gt;, 2 en &lt;code&gt;apigw-apis&lt;/code&gt; y 4 en &lt;code&gt;apigw-integrations&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;El Transit Gateway &lt;code&gt;hub&lt;/code&gt; (ASN 64512) tiene 4 attachments: la VPC &lt;code&gt;prod&lt;/code&gt;, la VPC &lt;code&gt;shared-services&lt;/code&gt;, la VPN &lt;code&gt;dc-vpn&lt;/code&gt; y el peering, que está en estado &lt;code&gt;pendingAcceptance&lt;/code&gt; y lleva el hallazgo &lt;code&gt;pending_acceptance&lt;/code&gt;. Su tabla de rutas por defecto tiene 6 rutas: &lt;code&gt;0.0.0.0/0 → shared-services&lt;/code&gt;, &lt;code&gt;10.0.0.0/16 → prod&lt;/code&gt;, &lt;code&gt;10.1.0.0/16 → shared-services&lt;/code&gt;, &lt;code&gt;10.20.0.0/16 →&lt;/code&gt; el peering de Frankfurt, &lt;code&gt;10.99.0.0/16&lt;/code&gt; como blackhole y &lt;code&gt;172.16.0.0/12 → dc-vpn&lt;/code&gt;. La segunda tabla, &lt;code&gt;hub-partners&lt;/code&gt;, tiene una ruta, &lt;code&gt;10.0.0.0/16 → prod&lt;/code&gt;, y el peering está asociado a ella.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flltp1usgq0uaw3e8qo5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flltp1usgq0uaw3e8qo5z.png" alt="El Transit Gateway hub: dos tarjetas de tablas de rutas a la izquierda, el gateway en el centro y cuatro cajas de attachment a la derecha" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;El Transit Gateway &lt;code&gt;hub&lt;/code&gt; de la cuenta simulada: sus dos tablas de rutas a la izquierda, sus cuatro attachments a la derecha y el peering sin aceptar con su estado entre paréntesis.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;«¿A dónde va esta ruta?» ahora es una tarjeta a la izquierda y una línea hasta una caja a la derecha.&lt;/p&gt;

&lt;p&gt;En la VPC &lt;code&gt;prod&lt;/code&gt; hay 6 security groups (&lt;code&gt;web&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, &lt;code&gt;bastion&lt;/code&gt;, &lt;code&gt;legacy-app&lt;/code&gt; y &lt;code&gt;default&lt;/code&gt;) y 15 filas de reglas. &lt;code&gt;bastion&lt;/code&gt; lleva &lt;code&gt;open_ingress:tcp/22&lt;/code&gt;. &lt;code&gt;legacy-app&lt;/code&gt; lleva &lt;code&gt;open_ingress:tcp/3389&lt;/code&gt; y &lt;code&gt;unused&lt;/code&gt;. De las 31 filas de &lt;code&gt;sg-attachments&lt;/code&gt;, 12 son &lt;code&gt;via_eni&lt;/code&gt;, 9 de ellas del grupo &lt;code&gt;default&lt;/code&gt;: cuando solo una interfaz de red delata que un grupo está asociado, la página y la tabla nombran la interfaz, no el recurso que hay detrás. La VPC &lt;code&gt;shared-services&lt;/code&gt; tiene 2 grupos y 4 reglas, y la tercera VPC 1 grupo y 1 regla.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe4he7o1zo9iy6pc8lj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe4he7o1zo9iy6pc8lj2.png" alt="Parte superior de la página de security groups de la VPC prod: una tarjeta por grupo a la izquierda y los recursos asociados a la derecha" width="799" height="736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La parte de arriba de la página de security groups de la VPC &lt;code&gt;prod&lt;/code&gt;. La página es alta, con una tarjeta por grupo y todos los recursos asociados, así que esto es un recorte. La &lt;a href="https://vpcatlas.com/assets/blog/focus/sg-prod-full.png" rel="noopener noreferrer"&gt;página completa&lt;/a&gt; está a un clic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Cada tarjeta enseña las primeras ocho reglas de entrada y las primeras cuatro de salida de su grupo. Una regla que nombra a otro grupo de la VPC es una línea entre las dos tarjetas, rotulada con el protocolo y los puertos: &lt;code&gt;app&lt;/code&gt; acepta el 22 desde &lt;code&gt;bastion&lt;/code&gt;, y &lt;code&gt;legacy-app&lt;/code&gt; acepta el 8080 desde &lt;code&gt;web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;La región tiene dos API. &lt;code&gt;prod-http&lt;/code&gt; es una API HTTP en &lt;code&gt;api.vpcatlas.com&lt;/code&gt; con el endpoint por defecto todavía activo, así que lleva &lt;code&gt;default_endpoint_open&lt;/code&gt;, y tiene un VPC link, &lt;code&gt;prod-link&lt;/code&gt;. &lt;code&gt;partner-rest&lt;/code&gt; es una API REST en &lt;code&gt;partners.vpcatlas.com&lt;/code&gt; con el endpoint por defecto desactivado y una web ACL regional, &lt;code&gt;prod-api&lt;/code&gt;, en su stage. &lt;code&gt;apigw-integrations&lt;/code&gt; tiene 4 filas: &lt;code&gt;GET /orders +1&lt;/code&gt; hacia la Lambda &lt;code&gt;prod-api&lt;/code&gt; (dos rutas que comparten una integración son una sola fila, y el &lt;code&gt;+1&lt;/code&gt; cuenta la otra), &lt;code&gt;ANY /app/{proxy+}&lt;/code&gt; por el VPC link &lt;code&gt;prod-link&lt;/code&gt; hasta el balanceador de carga &lt;code&gt;prod-web&lt;/code&gt;, &lt;code&gt;GET /status&lt;/code&gt; hacia &lt;code&gt;https://status.partner.example/health&lt;/code&gt; y &lt;code&gt;POST /orders&lt;/code&gt; hacia la Lambda &lt;code&gt;prod-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyy931wbl56jzbya0qwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyy931wbl56jzbya0qwa.png" alt="Las API de eu-west-1: las tarjetas de prod-http y partner-rest a la izquierda y, a la derecha, la Lambda, el VPC link con su balanceador y el host externo" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Las API de &lt;code&gt;eu-west-1&lt;/code&gt;: &lt;code&gt;prod-http&lt;/code&gt; lleva la línea &lt;code&gt;! default endpoint open&lt;/code&gt;, y a la derecha están la Lambda, el VPC link con su balanceador y el host externo.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Las rutas que no llevaban a ningún sitio
&lt;/h2&gt;

&lt;p&gt;Mientras preparaba estas figuras, las rutas del hub salieron sin destino. Todas decían &lt;code&gt;→ -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;El producto pide a AWS las rutas de cada tabla con &lt;code&gt;SearchTransitGatewayRoutes&lt;/code&gt; y lee el attachment de cada ruta de la respuesta. El simulador (moto) guarda el attachment de una ruta como un único objeto, mientras que la API real devuelve una lista, así que su serializador lo perdía y cada ruta salía con la lista de attachments vacía. En una cuenta real, las rutas ya tenían su destino desde el principio.&lt;/p&gt;

&lt;p&gt;El límite está en el simulador, y ahí lo arreglé: antes de arrancar moto, el script de la demo (&lt;code&gt;demo/fake_aws.py&lt;/code&gt;) envuelve la llamada que crea una ruta para que el attachment se guarde como lista.&lt;/p&gt;

&lt;p&gt;Un mock que responde con la forma correcta y el contenido equivocado se caza peor que uno que falla, y lo cazaron las tablas: una columna entera de &lt;code&gt;attachment_ids&lt;/code&gt; vacíos en &lt;code&gt;tgw-routes&lt;/code&gt; salta a la vista más que una flecha que acaba en un guion en una página.&lt;/p&gt;

&lt;h2&gt;
  
  
  Con un grafo guardado y en el dashboard
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--focus&lt;/code&gt; sobre un grafo guardado no necesita ninguna llamada a AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas diagram &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-out/vpcatlas-graph.json &lt;span class="nt"&gt;--focus&lt;/span&gt; tgw,apigw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el dashboard, &lt;code&gt;vpcatlas serve --focus ...&lt;/code&gt; o la variable &lt;code&gt;VPCATLAS_FOCUS&lt;/code&gt;, separada por comas, añade un tercer grupo a la barra lateral, llamado «Focus views». &lt;code&gt;/api/status&lt;/code&gt; informa de las vistas activas en &lt;code&gt;filters.focus&lt;/code&gt;. Docker Compose pasa &lt;code&gt;VPCATLAS_FOCUS&lt;/code&gt; tal cual, y el chart de Helm acepta &lt;code&gt;config.focus&lt;/code&gt; como lista. El resto está en la &lt;a href="https://vpcatlas.com/docs/dashboard/" rel="noopener noreferrer"&gt;documentación del dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que no hace
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Una página focus no tiene capa de cambios en el dashboard: «View changes on diagram» la salta.&lt;/li&gt;
&lt;li&gt;Una vista sin nada que enseñar no escribe página. Sin Transit Gateway no hay página &lt;code&gt;tgw&lt;/code&gt;, y sin API no hay página &lt;code&gt;apigw&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Un Transit Gateway compartido por una cuenta que no has escaneado se dibuja con lo que tus credenciales pueden leer: el gateway con su dueño y tus attachments a él. Y una tarjeta de tabla de rutas lista como mucho 12 rutas y termina con &lt;code&gt;+N more&lt;/code&gt;; si AWS anuncia más rutas de las que devuelve, la tarjeta termina con &lt;code&gt;(routes truncated by the scan)&lt;/code&gt; y &lt;code&gt;routes_truncated&lt;/code&gt; vale &lt;code&gt;yes&lt;/code&gt; en &lt;code&gt;tgw-routes&lt;/code&gt;. La tabla tiene todas las rutas que el escaneo leyó.&lt;/li&gt;
&lt;li&gt;Un grupo que solo delata una interfaz se lista a través de esa interfaz (&lt;code&gt;via_eni&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;En la página de API, un balanceador al que se llega por un VPC link y que además es destino directo de otra integración se dibuja una sola vez, y su arista cruza la columna de destinos. Lo dejo para la 0.9.1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un hallazgo como &lt;code&gt;open_ingress&lt;/code&gt; no siempre es un problema: el bastion de las figuras está abierto en el 22 a propósito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dime qué vista toca ahora
&lt;/h2&gt;

&lt;p&gt;La opción está en la &lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;documentación del CLI&lt;/a&gt;, el historial de versiones en el &lt;a href="https://vpcatlas.com/docs/changelog/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;, y el paquete en &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;. ¿Qué vista querrías después: balanceadores de carga, VPC endpoints, algo que sigues buscando por toda la página de región? Y ¿la regla &lt;code&gt;unused&lt;/code&gt; te parece acertada para tus cuentas, o tienes a propósito grupos que nada usa? Dímelo.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One service at the centre: Transit Gateway, security group and API Gateway pages</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Tue, 20 Oct 2026 05:54:34 +0000</pubDate>
      <link>https://dev.to/karinapp/one-service-at-the-centre-transit-gateway-security-group-and-api-gateway-pages-4obn</link>
      <guid>https://dev.to/karinapp/one-service-at-the-centre-transit-gateway-security-group-and-api-gateway-pages-4obn</guid>
      <description>&lt;p&gt;How VPC Atlas 0.9.0 draws one service per page, with its tables and findings, and what those pages still leave out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a page per service
&lt;/h2&gt;

&lt;p&gt;In a recent network review the questions were about one service at a time. Where does this Transit Gateway route go? Which security group is open to the world, and which one is attached to nothing? Where does each route of this API end up? VPC Atlas draws everything on the region page, which answers none of them well: the answer to each is on it somewhere, spread across the whole page.&lt;/p&gt;

&lt;p&gt;Version 0.9.0, published on 20 October 2026, adds &lt;code&gt;--focus&lt;/code&gt;: a page per service, two tables per view next to the inventory, and three findings the tool raises on those pages and tables. One small change isn't about focus: the pages of a VPC now show the free IPs of each subnet in its subtitle, as in &lt;code&gt;10.0.1.0/24 | 251 free IPs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The option
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas        &lt;span class="c"&gt;# or: curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1 &lt;span class="nt"&gt;--focus&lt;/span&gt; tgw,sg,apigw &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--focus VIEW&lt;/code&gt; works on &lt;code&gt;discover&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;diagram&lt;/code&gt; and &lt;code&gt;serve&lt;/code&gt;. You can repeat it or separate the views with commas. There are three views, and aliases such as &lt;code&gt;transit-gateway&lt;/code&gt; and &lt;code&gt;api-gateway&lt;/code&gt; are accepted.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tgw&lt;/code&gt; writes one page per Transit Gateway, your own or one shared with you, with its route tables, routes and attachments (a shared one is drawn from what your credentials can read, see the limits below). &lt;code&gt;sg&lt;/code&gt; writes one page per VPC, with its security groups, their rules and what they're attached to. &lt;code&gt;apigw&lt;/code&gt; writes one page per region, with its APIs, endpoints, domains and integration targets. The pages come out in draw.io and Lucid as &lt;code&gt;diagrams/vpcatlas-focus-&amp;lt;view&amp;gt;-&amp;lt;id&amp;gt;.&amp;lt;ext&amp;gt;&lt;/code&gt;, drawn by the same layout engine as the other pages (&lt;a href="https://vpcatlas.com/docs/outputs/diagrams/#focus-pages" rel="noopener noreferrer"&gt;focus pages&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;--focus&lt;/code&gt;, &lt;code&gt;--level&lt;/code&gt; defaults to the focus pages only; &lt;code&gt;--level account&lt;/code&gt;, &lt;code&gt;vpc&lt;/code&gt; or &lt;code&gt;all&lt;/code&gt; adds those pages. Without &lt;code&gt;--focus&lt;/code&gt; nothing changes. &lt;code&gt;--detail&lt;/code&gt; doesn't change a focus page, and &lt;code&gt;--tag&lt;/code&gt; narrows the graph first, so a resource the rules drop isn't on a focus page either.&lt;/p&gt;

&lt;p&gt;On &lt;code&gt;discover&lt;/code&gt; and &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;--focus&lt;/code&gt; also writes six tables next to the inventory: &lt;code&gt;tgw-attachments&lt;/code&gt;, &lt;code&gt;tgw-routes&lt;/code&gt;, &lt;code&gt;sg-rules&lt;/code&gt;, &lt;code&gt;sg-attachments&lt;/code&gt;, &lt;code&gt;apigw-apis&lt;/code&gt; and &lt;code&gt;apigw-integrations&lt;/code&gt;. Each one is a file called &lt;code&gt;vpcatlas-focus-&amp;lt;table&amp;gt;&lt;/code&gt; in every inventory format you selected (json, md, html, csv). The JSON has the schema id &lt;code&gt;vpcatlas-focus/1&lt;/code&gt;, and rows are sorted so the same graph gives the same bytes. The columns are in the &lt;a href="https://vpcatlas.com/docs/outputs/inventory/#focus-tables" rel="noopener noreferrer"&gt;focus tables&lt;/a&gt; section of the docs.&lt;/p&gt;

&lt;p&gt;Three findings appear on the pages and in the tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;open_ingress:&amp;lt;proto&amp;gt;/&amp;lt;ports&amp;gt;&lt;/code&gt;, on a security group that admits the world (sources 0.0.0.0/0 or ::/0) on a port other than 80 or 443. The card reads &lt;code&gt;! open to the world: tcp 22&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;unused&lt;/code&gt;, on a group that nothing uses and no rule names. The card reads &lt;code&gt;! not attached&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;default_endpoint_open&lt;/code&gt;, on an API with a custom domain whose default &lt;code&gt;execute-api&lt;/code&gt; endpoint is still enabled. The card reads &lt;code&gt;! default endpoint open&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fourth mark is on the Transit Gateway page. A peering attachment that nobody accepted shows &lt;code&gt;pending_acceptance&lt;/code&gt; in the attachments table, and its state in brackets on the line to its box.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unused&lt;/code&gt; isn't raised when a service that carries security groups was left out of the scan with &lt;code&gt;--exclude-services&lt;/code&gt;, or couldn't be read. A group that looks unused only because the scan never looked at Lambda or ECS would be a false alarm, and I'd rather the tool stay quiet than raise a wrong flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;The figures come from the simulated account of the repository (&lt;code&gt;demo/fake_aws.py&lt;/code&gt;, built on moto 5.2.4, the Python library that mocks AWS, behind a small shim), in &lt;code&gt;eu-west-1&lt;/code&gt;, with the released 0.9.0. I grew the account for this release so the three views had something to show. It now has a bastion instance in a public subnet with a group &lt;code&gt;bastion&lt;/code&gt; (SSH open to the world), a group &lt;code&gt;legacy-app&lt;/code&gt; with RDP open to the world and nothing attached to it, an HTTP API &lt;code&gt;prod-http&lt;/code&gt; and a REST API &lt;code&gt;partner-rest&lt;/code&gt;, a VPN &lt;code&gt;dc-vpn&lt;/code&gt; attached to the Transit Gateway &lt;code&gt;hub&lt;/code&gt;, a peering &lt;code&gt;hub-frankfurt&lt;/code&gt; that nobody has accepted, to a Transit Gateway in &lt;code&gt;eu-central-1&lt;/code&gt; of another account, and a second route table &lt;code&gt;hub-partners&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover -r eu-west-1 --focus tgw,sg,apigw --output-dir vpcatlas-out&lt;/code&gt; finds 113 resources and 0 issues (no call was denied or failed), and writes 5 focus pages: 1 for the Transit Gateway, 3 for security groups (the account has 3 VPCs) and 1 for the APIs of the region. The six tables have 7 rows in &lt;code&gt;tgw-routes&lt;/code&gt;, 4 in &lt;code&gt;tgw-attachments&lt;/code&gt;, 20 in &lt;code&gt;sg-rules&lt;/code&gt;, 31 in &lt;code&gt;sg-attachments&lt;/code&gt;, 2 in &lt;code&gt;apigw-apis&lt;/code&gt; and 4 in &lt;code&gt;apigw-integrations&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On the Transit Gateway page, the gateway &lt;code&gt;hub&lt;/code&gt; (ASN 64512) has 4 attachments: the VPC &lt;code&gt;prod&lt;/code&gt;, the VPC &lt;code&gt;shared-services&lt;/code&gt;, the VPN &lt;code&gt;dc-vpn&lt;/code&gt; and the peering, which is in state &lt;code&gt;pendingAcceptance&lt;/code&gt; and carries the mark &lt;code&gt;pending_acceptance&lt;/code&gt;. Its default route table has 6 routes: &lt;code&gt;0.0.0.0/0 → shared-services&lt;/code&gt;, &lt;code&gt;10.0.0.0/16 → prod&lt;/code&gt;, &lt;code&gt;10.1.0.0/16 → shared-services&lt;/code&gt;, &lt;code&gt;10.20.0.0/16 → the Frankfurt peering&lt;/code&gt;, &lt;code&gt;10.99.0.0/16&lt;/code&gt; as a blackhole, and &lt;code&gt;172.16.0.0/12 → dc-vpn&lt;/code&gt;. The second table, &lt;code&gt;hub-partners&lt;/code&gt;, has one route, &lt;code&gt;10.0.0.0/16 → prod&lt;/code&gt;, and the peering is associated to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flltp1usgq0uaw3e8qo5z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flltp1usgq0uaw3e8qo5z.png" alt="The page of the Transit Gateway hub: two route table cards on the left with their routes, the gateway in the middle and four attachment boxes on the right, the peering line reading peering (pendingAcceptance)" width="800" height="564"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Transit Gateway &lt;code&gt;hub&lt;/code&gt; of the simulated account: its two route tables on the left, its four attachments on the right, and the unaccepted peering with its state in brackets.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The VPC &lt;code&gt;prod&lt;/code&gt; has 6 groups (&lt;code&gt;web&lt;/code&gt;, &lt;code&gt;app&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, &lt;code&gt;bastion&lt;/code&gt;, &lt;code&gt;legacy-app&lt;/code&gt; and &lt;code&gt;default&lt;/code&gt;) and 15 rule rows. &lt;code&gt;bastion&lt;/code&gt; carries &lt;code&gt;open_ingress:tcp/22&lt;/code&gt;. &lt;code&gt;legacy-app&lt;/code&gt; carries &lt;code&gt;open_ingress:tcp/3389&lt;/code&gt; and &lt;code&gt;unused&lt;/code&gt;. Of the 31 rows in &lt;code&gt;sg-attachments&lt;/code&gt;, 12 are &lt;code&gt;via_eni&lt;/code&gt;, 9 of them on the group &lt;code&gt;default&lt;/code&gt;: when only a network interface shows that a group is attached, the page and the table name the interface, not the resource behind it. The VPC &lt;code&gt;shared-services&lt;/code&gt; has 2 groups and 4 rules, and the third VPC 1 group and 1 rule.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe4he7o1zo9iy6pc8lj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqe4he7o1zo9iy6pc8lj2.png" alt="The top of the page of the security groups of VPC prod: a card per group on the left, with the bastion and legacy-app findings, and the resources they are attached to on the right" width="799" height="736"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The top of the page of the security groups of VPC &lt;code&gt;prod&lt;/code&gt;. The page is tall, with a card per group and every attached resource, so this is a crop. The &lt;a href="https://vpcatlas.com/assets/blog/focus/sg-prod-full.png" rel="noopener noreferrer"&gt;full page&lt;/a&gt; has the rest.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each card shows the first eight inbound and four outbound rules of its group. A rule that names another group of the VPC is a line between the two cards, labelled with the protocol and the ports: &lt;code&gt;app&lt;/code&gt; accepts 22 from &lt;code&gt;bastion&lt;/code&gt;, and &lt;code&gt;legacy-app&lt;/code&gt; accepts 8080 from &lt;code&gt;web&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The region has two APIs. &lt;code&gt;prod-http&lt;/code&gt; is an HTTP API on &lt;code&gt;api.vpcatlas.com&lt;/code&gt; whose default endpoint is still enabled, so it carries &lt;code&gt;default_endpoint_open&lt;/code&gt;, and it has a VPC link &lt;code&gt;prod-link&lt;/code&gt;. &lt;code&gt;partner-rest&lt;/code&gt; is a REST API on &lt;code&gt;partners.vpcatlas.com&lt;/code&gt; with the default endpoint disabled and a regional web ACL &lt;code&gt;prod-api&lt;/code&gt; on its stage. &lt;code&gt;apigw-integrations&lt;/code&gt; has 4 rows: &lt;code&gt;GET /orders +1&lt;/code&gt; to the Lambda &lt;code&gt;prod-api&lt;/code&gt; (two routes that share one integration are one row, and the &lt;code&gt;+1&lt;/code&gt; counts the other), &lt;code&gt;ANY /app/{proxy+}&lt;/code&gt; through the VPC link &lt;code&gt;prod-link&lt;/code&gt; to the load balancer &lt;code&gt;prod-web&lt;/code&gt;, &lt;code&gt;GET /status&lt;/code&gt; to &lt;code&gt;https://status.partner.example/health&lt;/code&gt;, and &lt;code&gt;POST /orders&lt;/code&gt; to the Lambda &lt;code&gt;prod-api&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyy931wbl56jzbya0qwa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyyy931wbl56jzbya0qwa.png" alt="The page of the APIs of eu-west-1: the cards of prod-http and partner-rest with their domains, stages and web ACL, and to the right the Lambda, the VPC link with the load balancer behind it and the external HTTPS host" width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The APIs of &lt;code&gt;eu-west-1&lt;/code&gt;: &lt;code&gt;prod-http&lt;/code&gt; carries the line &lt;code&gt;! default endpoint open&lt;/code&gt;, and to the right are the Lambda, the VPC link with its load balancer and the external host.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The routes that pointed nowhere
&lt;/h2&gt;

&lt;p&gt;While I was making these figures, the routes of the hub came out with no target. Every route read &lt;code&gt;→ -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The product asks AWS for the routes of each route table with &lt;code&gt;SearchTransitGatewayRoutes&lt;/code&gt;, and it reads the attachment of every route in the answer. moto stores the attachment of a route as a single object where the real API returns a list, so its serializer dropped it, and every route came back with an empty list of attachments. Against a real account, the routes had their targets all along.&lt;/p&gt;

&lt;p&gt;The limit is in the simulator, so the fix went there: before moto starts, the demo script (&lt;code&gt;demo/fake_aws.py&lt;/code&gt;) wraps the call that creates a route so the attachment is stored as a list.&lt;/p&gt;

&lt;p&gt;A mock that answers with the right shape and the wrong content is harder to catch than one that fails, and it was the tables that caught it: a whole column of empty &lt;code&gt;attachment_ids&lt;/code&gt; in &lt;code&gt;tgw-routes&lt;/code&gt; is harder to skim past than an arrow to a dash on a page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache and dashboard
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--focus&lt;/code&gt; on a saved graph needs no AWS call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas diagram &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-out/vpcatlas-graph.json &lt;span class="nt"&gt;--focus&lt;/span&gt; tgw,apigw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the dashboard, &lt;code&gt;vpcatlas serve --focus ...&lt;/code&gt; or the variable &lt;code&gt;VPCATLAS_FOCUS&lt;/code&gt;, comma separated, adds a third sidebar group called "Focus views". &lt;code&gt;/api/status&lt;/code&gt; reports the active views under &lt;code&gt;filters.focus&lt;/code&gt;. Docker Compose passes &lt;code&gt;VPCATLAS_FOCUS&lt;/code&gt; through, and the Helm chart takes &lt;code&gt;config.focus&lt;/code&gt; as a list. The &lt;a href="https://vpcatlas.com/docs/dashboard/" rel="noopener noreferrer"&gt;dashboard docs&lt;/a&gt; have the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A focus page has no layer of changes in the dashboard: "View changes on diagram" skips it.&lt;/li&gt;
&lt;li&gt;A view with nothing to show writes no page. With no Transit Gateway there's no &lt;code&gt;tgw&lt;/code&gt; page, and with no API there's no &lt;code&gt;apigw&lt;/code&gt; page.&lt;/li&gt;
&lt;li&gt;A Transit Gateway shared by an account you didn't scan is drawn from what your credentials can read: the gateway with its owner and your attachments to it. And a route table card lists at most 12 routes and ends in &lt;code&gt;+N more&lt;/code&gt;; when AWS reports more routes than it returns, the card ends in &lt;code&gt;(routes truncated by the scan)&lt;/code&gt; and &lt;code&gt;routes_truncated&lt;/code&gt; is &lt;code&gt;yes&lt;/code&gt; in &lt;code&gt;tgw-routes&lt;/code&gt;. The table has every route the scan read.&lt;/li&gt;
&lt;li&gt;A group that only an interface reveals is listed through that interface (&lt;code&gt;via_eni&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;On the API page, a load balancer reached through a VPC link that is also the direct target of another integration is drawn once, and its edge crosses the column of targets. I've left that for 0.9.1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hit isn't always a problem: the bastion in the figures is open on 22 on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tell me which view is next
&lt;/h2&gt;

&lt;p&gt;The option is in the &lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;CLI docs&lt;/a&gt;, the version history in the &lt;a href="https://vpcatlas.com/docs/changelog/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;, and the package on &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;. Which view would you want next: load balancers, VPC endpoints, something else you keep chasing across a region page? And is the &lt;code&gt;unused&lt;/code&gt; rule right for your accounts, or do you keep groups on purpose that nothing uses? Tell me.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Una cuenta de AWS, varios entornos y los tags como único mapa</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Mon, 19 Oct 2026 11:24:29 +0000</pubDate>
      <link>https://dev.to/karinapp/una-cuenta-de-aws-varios-entornos-y-los-tags-como-unico-mapa-1ia</link>
      <guid>https://dev.to/karinapp/una-cuenta-de-aws-varios-entornos-y-los-tags-como-unico-mapa-1ia</guid>
      <description>&lt;p&gt;Cómo VPC Atlas dibuja y hace el inventario de un entorno de una cuenta de AWS compartida con su nuevo filtro por tag, y qué deja fuera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuando la cuenta es toda la empresa
&lt;/h2&gt;

&lt;p&gt;Muchos equipos no reparten su AWS en cuentas como hacen los diagramas de referencia. No lo defiendo: una cuenta por entorno es el consejo habitual. Esto es para las cuentas que ya existen. Una sola cuenta guarda prod, staging y un sandbox, y a veces varios equipos además. Lo que los distingue es un tag: &lt;code&gt;Environment=prod&lt;/code&gt;, &lt;code&gt;Team=payments&lt;/code&gt;. Si el etiquetado es coherente, los tags son el único mapa de qué pertenece a qué.&lt;/p&gt;

&lt;p&gt;Eso es incómodo para una herramienta que dibuja redes. VPC Atlas toma la cuenta como unidad y dibuja lo que encuentra, región por región. En una cuenta compartida eso significa prod junto a staging junto al sandbox, en una sola página. La página crece y dice poco: preguntabas por prod y la respuesta llega mezclada con el resto. VPC Atlas también escribe una página por VPC, y eso basta cuando cada entorno tiene la suya. No basta cuando dos entornos comparten VPC, ni cuando quieres que la página de región, el inventario y el dashboard salgan reducidos también.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw89bh1cijjkdzqaq8icc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw89bh1cijjkdzqaq8icc.png" alt="La página de eu-west-1 de la cuenta simulada: los servicios de edge, una VPC prod con nueve subnets en tres zonas, una VPC de servicios compartidos, una tercera VPC, el hub del Transit Gateway, la VPN a la oficina y un peering con otra cuenta" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La cuenta entera en una página: tres VPC, el hub, la VPN, el peering y la fila de edge.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La versión 0.8.0 añade &lt;code&gt;--tag&lt;/code&gt;. Te da el diagrama y el inventario de un entorno de una cuenta compartida, sin mover ni un solo recurso. Aquí cuento la opción, dos decisiones de diseño (una con un bug que encontré al preparar estas figuras), lo que desaparece bajo el filtro y cómo funciona con un grafo guardado y con el dashboard. La parte que yo leería dos veces es la que explica lo que desaparece. Hace falta la 0.8.1 o posterior; el motivo está en la historia de más abajo.&lt;/p&gt;

&lt;h2&gt;
  
  
  La opción
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas        &lt;span class="c"&gt;# o: curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1 &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--tag CLAVE=VALOR&lt;/code&gt; conserva los recursos que llevan ese tag con ese valor. &lt;code&gt;--tag CLAVE&lt;/code&gt; conserva los que llevan el tag con cualquier valor. La opción se puede repetir, y entonces deben cumplirse todas las reglas a la vez (un AND). En el valor, &lt;code&gt;*&lt;/code&gt; y &lt;code&gt;?&lt;/code&gt; son comodines. Claves y valores se comparan tal cual, mayúsculas incluidas, de modo que &lt;code&gt;environment=prod&lt;/code&gt; no encuentra nada en una cuenta que usa &lt;code&gt;Environment=prod&lt;/code&gt;. El valor se corta en el primer &lt;code&gt;=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tres formas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;payments
vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="s1"&gt;'Name=web-*'&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; Owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La primera es un equipo dentro de un entorno, la segunda una convención de nombres, la tercera todo lo que tiene dueño, sea cual sea. La opción funciona en &lt;code&gt;discover&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;diagram&lt;/code&gt; y &lt;code&gt;serve&lt;/code&gt; (&lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;documentación&lt;/a&gt;). Primero se excluyen los servicios con &lt;code&gt;--exclude-services&lt;/code&gt; (la opción que ya existía para dejar fuera servicios enteros), y después las reglas de tag deciden qué se queda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que medí
&lt;/h2&gt;

&lt;p&gt;Las cifras salen de la cuenta simulada de los artículos anteriores, en &lt;code&gt;eu-west-1&lt;/code&gt;, creada con &lt;code&gt;moto&lt;/code&gt;, la biblioteca de Python que imita AWS. La VPC prod (10.0.0.0/16) tiene nueve subnets en tres zonas, NAT gateways, un balanceador de carga, tres grupos de una sola instancia, endpoints, mount targets de EFS, una instancia de RDS, un clúster de ElastiCache &lt;code&gt;prod-cache&lt;/code&gt;, una Lambda &lt;code&gt;prod-api&lt;/code&gt; y un clúster de ECS &lt;code&gt;prod&lt;/code&gt; con el servicio &lt;code&gt;prod-worker&lt;/code&gt;. A su lado hay una VPC de servicios compartidos con endpoints y una regla de Route 53 Resolver, una tercera VPC, un Transit Gateway &lt;code&gt;hub&lt;/code&gt;, una VPN a una oficina, un peering con una VPC de otra cuenta, y los servicios de edge y de cuenta (CloudFront, la hosted zone, el certificado, Shield, WAF, S3, DynamoDB, CloudTrail, Config, GuardDuty y otros).&lt;/p&gt;

&lt;p&gt;Los tags siguen una convención: la mayoría de los recursos de prod llevan &lt;code&gt;Environment=prod&lt;/code&gt; y los compartidos &lt;code&gt;Environment=shared&lt;/code&gt;. El clúster de ECS &lt;code&gt;prod&lt;/code&gt; y su servicio &lt;code&gt;prod-worker&lt;/code&gt;, la web ACL, el hub, la VPN, el customer gateway de la oficina, las interfaces de red, las ACL de red y las listas de prefijos no llevan ningún tag &lt;code&gt;Environment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover -r eu-west-1 --output-dir vpcatlas-all&lt;/code&gt; encuentra 100 recursos y 0 incidencias. Después, sobre el grafo guardado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-all/vpcatlas-graph.json &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--format&lt;/span&gt; csv &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eso escribe 34 filas de 100: 1 de 3 VPC, 9 de 14 subnets, 5 de 9 tablas de rutas, 3 de 7 security groups, los 3 grupos de instancias, los 3 NAT gateways, los 3 endpoints, y uno de cada para el balanceador, el target group, la instancia de RDS, el sistema de ficheros EFS, el internet gateway, el virtual private gateway y el peering.&lt;/p&gt;

&lt;p&gt;El diagrama sale del mismo grafo guardado y de la misma regla:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas diagram &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-all/vpcatlas-graph.json &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmklrjh6hyy3ddrqnxscr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmklrjh6hyy3ddrqnxscr.png" alt="La misma página con --tag Environment=prod: solo la VPC prod con sus nueve subnets, las tres instancias, los NAT gateways, los endpoints, el balanceador, la base de datos, los mount targets de EFS, el internet gateway, el virtual private gateway y el peering con la otra cuenta" width="800" height="756"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La misma cuenta con &lt;code&gt;--tag Environment=prod&lt;/code&gt;: 34 de los 100 recursos, todos con &lt;code&gt;Environment=prod&lt;/code&gt;, más el marco del que cuelgan.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;El peering está porque lleva &lt;code&gt;Environment=prod&lt;/code&gt; él mismo; el dibujo lo cuelga de la VPC prod. La ejecución filtrada imprime un aviso, y quería que lo hiciera: «Working from a partial graph: only resources matching Environment=prod are kept (--tag).»&lt;/p&gt;

&lt;h2&gt;
  
  
  Conservar el marco
&lt;/h2&gt;

&lt;p&gt;La forma obvia de filtrar es tirar todo lo que no coincide. Pero un NAT gateway sin subnet, sin VPC y sin región no tiene dónde ponerse, y no se puede dibujar. Así que un recurso que se queda conserva su marco: su cuenta, su región, y la VPC y las subnets de las que cuelga. Una subnet que coincide trae consigo su VPC, tenga tag o no.&lt;/p&gt;

&lt;p&gt;La dirección contraria también pedía una decisión, y elegí la más estricta. Una VPC que coincide no arrastra sus subnets ni sus cargas de trabajo; tienen que coincidir por sí mismas. Si una VPC lleva &lt;code&gt;Environment=prod&lt;/code&gt; y una de sus subnets &lt;code&gt;Environment=staging&lt;/code&gt;, la subnet de staging se queda fuera del diagrama de prod, salvo que dentro haya algo con tag prod: entonces vuelve como marco de ese recurso. Prefiero un diagrama demasiado pequeño a uno que incluya staging sin avisar, que es justo lo que esta opción quiere evitar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los grupos de instancias que desaparecieron
&lt;/h2&gt;

&lt;p&gt;VPC Atlas no dibuja las instancias EC2 una a una. Las dibuja en grupos: por Auto Scaling group, por &lt;code&gt;Name&lt;/code&gt; o por subnet. Había que decidir qué tags lleva un grupo. En la 0.8.0 un grupo solo tenía su tag &lt;code&gt;Name&lt;/code&gt;, o los tags de su Auto Scaling group.&lt;/p&gt;

&lt;p&gt;Lo descubrí preparando las figuras de este artículo. Lancé &lt;code&gt;--tag Environment=prod&lt;/code&gt; contra la cuenta simulada esperando la VPC prod con sus tres instancias, y las instancias no estaban: 3 de 3 grupos fuera. Las instancias llevan &lt;code&gt;Environment=prod&lt;/code&gt;; los grupos no, porque un grupo solo tenía su nombre. El filtro hizo lo que yo había escrito, y lo que yo había escrito tiraba todo grupo de instancias cuyos tags estaban en las instancias y no en su nombre ni en su Auto Scaling group.&lt;/p&gt;

&lt;p&gt;El arreglo está en la 0.8.1, publicada el mismo día. Ahora un grupo lleva los tags que todos sus miembros comparten con el mismo valor, más los del propio Auto Scaling group y su &lt;code&gt;Name&lt;/code&gt;. Un tag que solo tienen algunos miembros no va en el grupo, porque no puedo llamar prod a un grupo si una de sus instancias no lo es. Con la 0.8.1 los tres grupos han vuelto, y la segunda figura se dibujó con esa versión. Si ejecutaste &lt;code&gt;--tag&lt;/code&gt; con la 0.8.0 en una cuenta con instancias, vuelve a escanear con la 0.8.1: un fichero de grafo guardado con la 0.8.0 conserva los tags antiguos de los grupos, y &lt;code&gt;--cache&lt;/code&gt; no los recupera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que desaparece, y por qué
&lt;/h2&gt;

&lt;p&gt;De los 100 recursos, 66 quedan fuera. Quince están en tipos que se conservan en parte: 2 de 3 VPC, 5 de 14 subnets, 4 de 9 tablas de rutas y 4 de 7 security groups (5 de los 15 llevan &lt;code&gt;Environment=shared&lt;/code&gt; y los otros 10 no llevan ningún tag). Los otros 51 son de 24 tipos que desaparecen por completo, y se reparten en tres grupos.&lt;/p&gt;

&lt;p&gt;El grupo más grande son recursos que no llevan tag &lt;code&gt;Environment&lt;/code&gt; en esta cuenta, así que el filtro hace bien en descartarlos: los servicios de edge (CloudFront, la hosted zone, el certificado, Shield), CloudTrail, Config y GuardDuty, el Transit Gateway con sus attachments y su tabla de rutas, la VPN y el customer gateway, 19 interfaces de red, 3 ACL de red y 6 listas de prefijos. Son huecos en el etiquetado de la cuenta. Por eso &lt;code&gt;prod-vgw&lt;/code&gt; en la segunda figura acaba en el borde de la VPC: su conexión VPN no lleva tag. Un rótulo como &lt;code&gt;+4 ENIs&lt;/code&gt; en la cabecera de una subnet es solo un contador que lleva la propia subnet; las interfaces en sí han desaparecido. El filtro solo muestra esos huecos como una ausencia; para listarlos, escribe el CSV con y sin &lt;code&gt;--tag&lt;/code&gt; y compara la columna &lt;code&gt;id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Después vienen los servicios cuyos colectores no registran ningún tag: S3 y DynamoDB, y también WAFv2 (la web ACL) y Route 53 Resolver (los endpoints y la regla). La herramienta no puede saber si alguno lleva tag, así que ninguno coincide. Los stubs (recursos de otras cuentas a los que se refiere la tuya) y los resúmenes de cuenta están en la misma situación, aunque ninguno está entre estos 100.&lt;/p&gt;

&lt;p&gt;El último grupo es un límite de la propia opción. Dos recursos están etiquetados &lt;code&gt;Environment=prod&lt;/code&gt; en AWS y aun así desaparecen: la Lambda &lt;code&gt;prod-api&lt;/code&gt; y el clúster de ElastiCache &lt;code&gt;prod-cache&lt;/code&gt;. Sus colectores todavía no leen tags. Así que un diagrama de prod hecho con &lt;code&gt;--tag&lt;/code&gt; no tiene una función ni una caché que cualquiera del equipo llamaría prod. El clúster de ECS &lt;code&gt;prod&lt;/code&gt; y su servicio &lt;code&gt;prod-worker&lt;/code&gt; también desaparecen, pero en esta cuenta no llevan tag &lt;code&gt;Environment&lt;/code&gt; (el clúster solo tiene &lt;code&gt;Name&lt;/code&gt;), así que no sirven para enseñar este hueco. La &lt;a href="https://vpcatlas.com/docs/coverage/#resources-without-tags" rel="noopener noreferrer"&gt;página de cobertura&lt;/a&gt; recoge este hueco de la herramienta. Mientras esos colectores no lean tags, trata el diagrama como la red etiquetada y revisa a mano esos servicios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caché, dashboard y una regla sobre el fichero
&lt;/h2&gt;

&lt;p&gt;Un escaneo con &lt;code&gt;--tag&lt;/code&gt; guarda el grafo reducido y anota las reglas en sus metadatos. Una ejecución posterior con &lt;code&gt;--cache&lt;/code&gt; sobre ese fichero imprime el aviso de grafo parcial y nombra las reglas, para que medio grafo no pase por uno entero. Una ejecución con &lt;code&gt;--cache&lt;/code&gt; y &lt;code&gt;--tag&lt;/code&gt; reduce el grafo cargado de la misma manera y deja el fichero como estaba. La consecuencia práctica: si quieres prod hoy y el sandbox mañana, escanea una vez sin &lt;code&gt;--tag&lt;/code&gt; y filtra el grafo guardado cada vez. Escanea con &lt;code&gt;--tag&lt;/code&gt; solo cuando quieras el fichero reducido.&lt;/p&gt;

&lt;p&gt;El dashboard toma las mismas reglas de &lt;code&gt;--tag&lt;/code&gt; en &lt;code&gt;vpcatlas serve&lt;/code&gt;, o de la variable &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt;, separadas por comas, por ejemplo &lt;code&gt;Environment=prod,Owner&lt;/code&gt;. Un valor que contenga una coma necesita la línea de comandos. &lt;code&gt;/api/status&lt;/code&gt; lista las reglas activas en &lt;code&gt;filters.tags&lt;/code&gt;. Docker Compose pasa &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt; tal cual, y el chart de Helm acepta &lt;code&gt;config.tags&lt;/code&gt; como lista. Con eso es posible un dashboard por entorno con las mismas credenciales, cada uno arrancado con su propia &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt;. El resto está en la &lt;a href="https://vpcatlas.com/docs/dashboard/" rel="noopener noreferrer"&gt;documentación del dashboard&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que no hace
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Filtra solo por tags. Una subnet sin tag se queda fuera del diagrama de prod aunque todas las de al lado entren, salvo que contenga un recurso que coincida.&lt;/li&gt;
&lt;li&gt;No puede usar los tags que los colectores no leen: por ahora, los de Lambda, ECS, ElastiCache, WAFv2, Route 53 Resolver, S3 y DynamoDB. Los tags salen de las mismas llamadas describe que los colectores ya hacen. La API de Resource Groups Tagging cubriría el resto en una sola llamada; todavía no está conectada.&lt;/li&gt;
&lt;li&gt;No hay modo que ignore mayúsculas, así que dos grafías de un mismo entorno son dos entornos.&lt;/li&gt;
&lt;li&gt;Reduce el grafo después del escaneo. El escaneo sigue leyendo toda la cuenta con los mismos permisos, así que &lt;code&gt;--tag&lt;/code&gt; es una vista, no un límite.&lt;/li&gt;
&lt;li&gt;Los 66 recursos que quedaron fuera son lo que pierde esta cuenta simulada; en la tuya serán otros.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dime qué falta
&lt;/h2&gt;

&lt;p&gt;La opción está en la &lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;documentación del CLI&lt;/a&gt;, el historial de versiones en el &lt;a href="https://vpcatlas.com/docs/changelog/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;, y el paquete en &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;. Si trabajas con una cuenta compartida, me gustaría saber qué tag usas de verdad para repartirla, y si un AND de reglas &lt;code&gt;CLAVE=VALOR&lt;/code&gt; te basta o necesitabas un OR. Dime qué servicio buscaste en un diagrama filtrado y no encontraste, para que lea sus tags a continuación. Y si te serviría que la salida dijera qué recursos dejó fuera el filtro, y si cada uno no tenía tag o está en un servicio cuyos tags todavía no leo, dímelo; hoy no lo dice.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>One AWS account, several environments, and tags as the only map</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Mon, 19 Oct 2026 11:23:19 +0000</pubDate>
      <link>https://dev.to/karinapp/one-aws-account-several-environments-and-tags-as-the-only-map-26hp</link>
      <guid>https://dev.to/karinapp/one-aws-account-several-environments-and-tags-as-the-only-map-26hp</guid>
      <description>&lt;p&gt;How VPC Atlas draws and inventories one environment of a shared AWS account with its new tag filter, and what it leaves out.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the account is the whole company
&lt;/h2&gt;

&lt;p&gt;Plenty of teams don't split their AWS estate into accounts the way the reference diagrams do. I'm not arguing for it: one account per environment is the usual advice. This is for the accounts that exist. One account holds prod, staging and a sandbox, sometimes several teams as well. What tells them apart is a tag: &lt;code&gt;Environment=prod&lt;/code&gt;, &lt;code&gt;Team=payments&lt;/code&gt;. If the tagging is consistent, the tags are the only map of what belongs to what.&lt;/p&gt;

&lt;p&gt;That's awkward for a tool that draws networks. VPC Atlas takes the account as its unit and draws what it finds, region by region. In a shared account that means prod next to staging next to the sandbox, on one page. The page gets big and says little, because your question was about prod and the answer is mixed in with the rest. VPC Atlas also writes one page per VPC, which is enough when every environment has its own. It isn't when two environments share a VPC, or when you want the region page, the inventory and the dashboard narrowed too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw89bh1cijjkdzqaq8icc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw89bh1cijjkdzqaq8icc.png" alt="The eu-west-1 page of the simulated account: the edge services, a prod VPC with nine subnets in three zones, a shared-services VPC, a third VPC, the Transit Gateway hub, the VPN to the office and a peering to another account" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The whole account on one page: three VPCs, the hub, the VPN, the peering and the edge row.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Version 0.8.0 adds &lt;code&gt;--tag&lt;/code&gt;. It gives you the diagram and the inventory of one environment of a shared account, without moving a single resource. This post covers the option, two design decisions (one with a bug I found while making these figures), what disappears under the filter, and how it works with a saved graph and the dashboard. The part I'd read twice is the one about what disappears. You need 0.8.1 or later; the reason is in the story below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The option
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas        &lt;span class="c"&gt;# or: curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;-r&lt;/span&gt; eu-west-1 &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--tag KEY=VALUE&lt;/code&gt; keeps the resources that carry that tag with that value. &lt;code&gt;--tag KEY&lt;/code&gt; keeps the ones that carry the tag with any value. The option can be repeated, and then every rule must hold (an AND). In the value, &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt; are wildcards. Keys and values are compared exactly, case included, so &lt;code&gt;environment=prod&lt;/code&gt; matches nothing in an account that spells it &lt;code&gt;Environment=prod&lt;/code&gt;. The value is split at the first &lt;code&gt;=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three shapes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;payments
vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="s1"&gt;'Name=web-*'&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;--tag&lt;/span&gt; Owner
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first is one team inside one environment, the second a naming convention, the third everything that has an owner at all. The option works on &lt;code&gt;discover&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;diagram&lt;/code&gt; and &lt;code&gt;serve&lt;/code&gt; (&lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). Services are excluded first, with &lt;code&gt;--exclude-services&lt;/code&gt; (the existing option that leaves whole services out), and then the tag rules judge what stays.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;The figures come from the simulated account of the earlier posts, in &lt;code&gt;eu-west-1&lt;/code&gt;, built with &lt;code&gt;moto&lt;/code&gt;, the Python library that mocks AWS. The prod VPC (10.0.0.0/16) has nine subnets in three zones, NAT gateways, a load balancer, three single-instance groups, endpoints, EFS mount targets, an RDS instance, an ElastiCache cluster &lt;code&gt;prod-cache&lt;/code&gt;, a Lambda &lt;code&gt;prod-api&lt;/code&gt; and an ECS cluster &lt;code&gt;prod&lt;/code&gt; with the service &lt;code&gt;prod-worker&lt;/code&gt;. Next to it sit a shared-services VPC with Route 53 Resolver endpoints and a rule, a third VPC, a Transit Gateway &lt;code&gt;hub&lt;/code&gt;, a VPN to an office, a peering to a VPC of another account, and the edge and account services (CloudFront, the hosted zone, the certificate, Shield, WAF, S3, DynamoDB, CloudTrail, Config, GuardDuty and so on).&lt;/p&gt;

&lt;p&gt;Tags follow a convention: most prod resources carry &lt;code&gt;Environment=prod&lt;/code&gt;, the shared ones &lt;code&gt;Environment=shared&lt;/code&gt;. The ECS cluster &lt;code&gt;prod&lt;/code&gt; and its service &lt;code&gt;prod-worker&lt;/code&gt;, the web ACL, the hub, the VPN, the customer gateway of the office, the network interfaces, the network ACLs and the prefix lists carry no &lt;code&gt;Environment&lt;/code&gt; tag.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover -r eu-west-1 --output-dir vpcatlas-all&lt;/code&gt; finds 100 resources and 0 issues. Then, on the saved graph:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-all/vpcatlas-graph.json &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--format&lt;/span&gt; csv &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes 34 rows out of 100: 1 of 3 VPCs, 9 of 14 subnets, 5 of 9 route tables, 3 of 7 security groups, all 3 instance groups, the 3 NAT gateways, the 3 endpoints, and one each of load balancer, target group, RDS instance, EFS file system, internet gateway, virtual private gateway and peering.&lt;/p&gt;

&lt;p&gt;The diagram comes from the same saved graph and the same rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas diagram &lt;span class="nt"&gt;--cache&lt;/span&gt; vpcatlas-all/vpcatlas-graph.json &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="nt"&gt;--output-dir&lt;/span&gt; vpcatlas-prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmklrjh6hyy3ddrqnxscr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmklrjh6hyy3ddrqnxscr.png" alt="The same page with --tag Environment=prod: only the prod VPC with its nine subnets, the three instances, the NAT gateways, the endpoints, the load balancer, the database, the EFS mount targets, the internet and virtual private gateways, and the peering to the other account" width="800" height="756"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same account with &lt;code&gt;--tag Environment=prod&lt;/code&gt;: 34 of the 100 resources, every one tagged &lt;code&gt;Environment=prod&lt;/code&gt;, plus the frame they hang from.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The peering is there because it carries &lt;code&gt;Environment=prod&lt;/code&gt; itself; the drawing hangs it from the prod VPC. The filtered run prints a warning, and I wanted it to: "Working from a partial graph: only resources matching Environment=prod are kept (--tag)."&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the frame
&lt;/h2&gt;

&lt;p&gt;The obvious way to filter is to throw away everything that doesn't match. A NAT gateway with no subnet, no VPC and no region has nowhere to sit, though, and can't be drawn. So a kept resource keeps its frame: its account, its region, and the VPC and subnets it hangs from. A subnet that matches brings its VPC along, tagged or not.&lt;/p&gt;

&lt;p&gt;The other direction needed a decision too, and I chose the stricter one. A VPC that matches doesn't drag in its subnets or its workloads; they have to match on their own. If a VPC is tagged &lt;code&gt;Environment=prod&lt;/code&gt; and one of its subnets &lt;code&gt;Environment=staging&lt;/code&gt;, the staging subnet stays out of the prod diagram, unless something tagged prod sits in it: then it comes back as that resource's frame. I'd rather have a diagram that's too small than one that quietly includes staging, which is what the option exists to avoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The instance groups that went missing
&lt;/h2&gt;

&lt;p&gt;VPC Atlas doesn't draw EC2 instances one by one. It draws them as groups: by Auto Scaling group, by &lt;code&gt;Name&lt;/code&gt;, or by subnet. So I had to decide what tags a group carries. In 0.8.0 a group had only its &lt;code&gt;Name&lt;/code&gt; tag, or the tags of its Auto Scaling group.&lt;/p&gt;

&lt;p&gt;I found out while making the figures for this post. I ran &lt;code&gt;--tag Environment=prod&lt;/code&gt; against the simulated account, expecting the prod VPC with its three instances, and the instances weren't there: 3 of 3 groups gone. The instances carry &lt;code&gt;Environment=prod&lt;/code&gt;; the groups didn't, because a group only had its name. The filter did what I'd written, and what I'd written dropped every instance group whose tags sat on the instances and not on the group's name or its Auto Scaling group.&lt;/p&gt;

&lt;p&gt;The fix is in 0.8.1, released the same day. A group now carries the tags that every member shares with the same value, plus the Auto Scaling group's own tags and its &lt;code&gt;Name&lt;/code&gt;. A tag only some members have isn't on the group, because I can't call the group prod when one of its instances isn't. With 0.8.1 the three groups are back, and the second figure was drawn with that version. If you ran &lt;code&gt;--tag&lt;/code&gt; with 0.8.0 on an account with instances, scan again with 0.8.1: a graph file saved by 0.8.0 still has the old group tags, so &lt;code&gt;--cache&lt;/code&gt; won't bring the groups back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What disappears, and why
&lt;/h2&gt;

&lt;p&gt;Of the 100 resources, 66 are gone. Fifteen of them are in types that stay in part: 2 of 3 VPCs, 5 of 14 subnets, 4 of 9 route tables and 4 of 7 security groups (5 of the 15 are tagged &lt;code&gt;Environment=shared&lt;/code&gt;, the other 10 carry no tags). The other 51 belong to 24 types that disappear completely, and they fall into three groups.&lt;/p&gt;

&lt;p&gt;The biggest group is resources that carry no &lt;code&gt;Environment&lt;/code&gt; tag in this account, so the filter is right to drop them: the edge services (CloudFront, the hosted zone, the certificate, Shield), CloudTrail, Config and GuardDuty, the Transit Gateway with its attachments and route table, the VPN and the customer gateway, 19 network interfaces, 3 network ACLs and 6 prefix lists. Those are gaps in the account's tagging. That's why &lt;code&gt;prod-vgw&lt;/code&gt; in the second figure ends at the edge of the VPC: its VPN connection carries no tag. A label like &lt;code&gt;+4 ENIs&lt;/code&gt; in a subnet header is just a count the subnet carries; the interfaces themselves are gone. The filter shows those gaps only as an absence, so to list them, write the CSV with and without &lt;code&gt;--tag&lt;/code&gt; and compare the &lt;code&gt;id&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;Next come services whose collectors record no tags at all: S3 and DynamoDB, and also WAFv2 (the web ACL) and Route 53 Resolver (the endpoints and the rule). The tool can't tell whether any of them is tagged, so none matches. Stubs (resources of other accounts that yours refer to) and the account summaries are in the same position, though none is among these 100.&lt;/p&gt;

&lt;p&gt;The last group is a limit of the option itself. Two resources are tagged &lt;code&gt;Environment=prod&lt;/code&gt; in AWS and still vanish: the Lambda &lt;code&gt;prod-api&lt;/code&gt; and the ElastiCache cluster &lt;code&gt;prod-cache&lt;/code&gt;. Their collectors don't read tags yet. So a prod diagram made with &lt;code&gt;--tag&lt;/code&gt; lacks a function and a cache that anyone on the team would call prod. The ECS cluster &lt;code&gt;prod&lt;/code&gt; and its service &lt;code&gt;prod-worker&lt;/code&gt; go too, but in this account they carry no &lt;code&gt;Environment&lt;/code&gt; tag (the cluster has only a &lt;code&gt;Name&lt;/code&gt;), so they can't show the gap. The &lt;a href="https://vpcatlas.com/docs/coverage/#resources-without-tags" rel="noopener noreferrer"&gt;coverage page&lt;/a&gt; lists this gap in the tool. Until those collectors read tags, treat the diagram as the tagged network and check those services by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache, dashboard and a rule about the file
&lt;/h2&gt;

&lt;p&gt;A scan run with &lt;code&gt;--tag&lt;/code&gt; saves the narrowed graph, and it records the rules in its metadata. A later &lt;code&gt;--cache&lt;/code&gt; run on that file prints the partial-graph warning and names the rules, so a half graph can't pass for a whole one. A &lt;code&gt;--cache&lt;/code&gt; run with &lt;code&gt;--tag&lt;/code&gt; narrows the loaded graph the same way and leaves the file as it is. The practical consequence: if you want prod today and the sandbox tomorrow, scan once without &lt;code&gt;--tag&lt;/code&gt; and filter the saved graph each time. Scan with &lt;code&gt;--tag&lt;/code&gt; only when you want the narrow file.&lt;/p&gt;

&lt;p&gt;The dashboard takes the same rules from &lt;code&gt;--tag&lt;/code&gt; on &lt;code&gt;vpcatlas serve&lt;/code&gt;, or from the &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt; variable, comma separated, for example &lt;code&gt;Environment=prod,Owner&lt;/code&gt;. A value that contains a comma needs the command line. &lt;code&gt;/api/status&lt;/code&gt; lists the active rules under &lt;code&gt;filters.tags&lt;/code&gt;. Docker Compose passes &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt; through, and the Helm chart takes &lt;code&gt;config.tags&lt;/code&gt; as a list. That makes one dashboard per environment possible from the same credentials, each started with its own &lt;code&gt;VPCATLAS_TAGS&lt;/code&gt;. The &lt;a href="https://vpcatlas.com/docs/dashboard/" rel="noopener noreferrer"&gt;dashboard docs&lt;/a&gt; cover the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It filters on tags only. A subnet with no tag stays out of the prod diagram even if every neighbour is in, unless a resource that matches sits in it.&lt;/li&gt;
&lt;li&gt;It can't use tags the collectors don't read: Lambda, ECS, ElastiCache, WAFv2, Route 53 Resolver, S3 and DynamoDB, at the moment. Tags come from the same describe calls the collectors already make. The Resource Groups Tagging API would cover the rest in one call; it isn't wired in yet.&lt;/li&gt;
&lt;li&gt;There's no case-insensitive mode, so two spellings of one environment are two environments.&lt;/li&gt;
&lt;li&gt;It narrows the graph after the scan. The scan still reads the whole account with the same permissions, so &lt;code&gt;--tag&lt;/code&gt; is a view, not a boundary.&lt;/li&gt;
&lt;li&gt;The 66 resources that went are what this simulated account loses; yours will differ.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tell me what's missing
&lt;/h2&gt;

&lt;p&gt;The option is in the &lt;a href="https://vpcatlas.com/docs/cli/discover/" rel="noopener noreferrer"&gt;CLI docs&lt;/a&gt;, the version history in the &lt;a href="https://vpcatlas.com/docs/changelog/" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;, and the package on &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;. If you run a shared account, I'd like to know which tag you actually use to split it, and whether an AND of &lt;code&gt;KEY=VALUE&lt;/code&gt; rules is enough or you needed an OR. Tell me which service you looked for in a filtered diagram and didn't find, so I can read its tags next. And if you'd want the output to say which resources the filter left out, and whether each was untagged or in a service whose tags I don't read yet, tell me; today it doesn't.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>devops</category>
      <category>opensource</category>
    </item>
    <item>
      <title>El inventario de red de AWS, ahora también en CSV para filtrarlo</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 23:35:43 +0000</pubDate>
      <link>https://dev.to/karinapp/el-inventario-de-red-de-aws-ahora-tambien-en-csv-para-filtrarlo-24ch</link>
      <guid>https://dev.to/karinapp/el-inventario-de-red-de-aws-ahora-tambien-en-csv-para-filtrarlo-24ch</guid>
      <description>&lt;p&gt;Qué contiene el inventario de VPC Atlas, los cuatro formatos en que sale y por qué hice el CSV con una fila plana por recurso.&lt;/p&gt;

&lt;h2&gt;
  
  
  El diagrama es la foto, el inventario es la lista
&lt;/h2&gt;

&lt;p&gt;Un diagrama te cuenta cómo es una red. Sirve poco para responder a «qué subnets son públicas en eu-west-1», a «qué ha cambiado desde el mes pasado» o a «mándame todas las VPC con sus CIDR». En una imagen no se puede buscar, y nadie compara dos a ojo con garantías. Para esas preguntas quería una lista: un único documento con todo lo que encontró el escaneo, que una persona pueda filtrar y un script pueda leer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fneprna8p24148a65md1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fneprna8p24148a65md1b.png" alt="El diagrama de la VPC prod de la cuenta simulada en draw.io: nueve subnets en tres zonas, tablas de rutas, NAT gateways y flujos numerados" width="800" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La foto: la VPC prod de la cuenta simulada, tal como la dibuja VPC Atlas. Cada recurso dibujado aquí es una fila del inventario de más abajo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;VPC Atlas ya escribía ese documento en JSON, Markdown y HTML. La versión 0.7.0 le suma una cuarta forma, el CSV. Aquí cuento qué contiene el inventario, para quién es cada formato, cómo se obtiene, tres maneras de filtrarlo y las decisiones que tomé en el CSV, entre ellas la que te mete un apóstrofo suelto en las celdas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué contiene el documento
&lt;/h2&gt;

&lt;p&gt;El inventario es la cuenta escrita como un árbol. De cada cuenta auditada cuelgan sus regiones. De una región cuelgan sus VPC y los recursos que no están en ninguna VPC, como los Transit Gateways y los peerings. Una VPC contiene sus subnets y los recursos que no están en ninguna subnet, como las tablas de rutas y los security groups. Una subnet contiene lo que se coloca en ella, como los NAT gateways y los balanceadores de carga, y trae su zona de disponibilidad, su CIDR y un nivel: pública cuando la ruta por defecto sale por un internet gateway; si no, privada, aislada o desconocida.&lt;/p&gt;

&lt;p&gt;Junto al árbol de red, cada cuenta tiene una capa de servicios en cinco familias: DNS y edge, API y balanceo, cómputo y contenedores, datos y almacenamiento, y servicios de cuenta como CloudTrail y GuardDuty. Cuando el escaneo no pudo leer un valor de los servicios de cuenta, el fichero pone &lt;code&gt;not read&lt;/code&gt;. Eso no es lo mismo que cero o &lt;code&gt;no&lt;/code&gt;, así que una llamada denegada nunca se lee como una cuenta vacía.&lt;/p&gt;

&lt;p&gt;Después vienen las partes que cruzan cuentas. Las cuentas que el escaneo vio pero para las que no tiene credenciales salen como cuentas externas, cada enlace que cruza el límite de una cuenta tiene su propia entrada, y las llamadas que fallaron al descubrir salen como incidencias (&lt;code&gt;issues&lt;/code&gt;). Un resumen con los recuentos abre el documento. Todos los recursos llevan los mismos campos: id, nombre, ARN, estado, sus detalles, sus tags, si el dueño es una cuenta que no auditaste y qué cuentas auditadas lo vieron.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuatro formatos, cuatro lectores
&lt;/h2&gt;

&lt;p&gt;El JSON, en &lt;code&gt;vpcatlas-inventory.json&lt;/code&gt;, es para máquinas y para &lt;code&gt;jq&lt;/code&gt;; su identificador de esquema es &lt;code&gt;vpcatlas-inventory/1&lt;/code&gt;. El Markdown es para una pull request o una wiki: el mismo documento en tablas, con los detalles cortados a 240 caracteres por celda. El HTML es una sola página autocontenida con un cuadro de búsqueda que filtra las filas por id, nombre, tipo o cuenta, y es el que mandaría a alguien que no va a abrir una terminal. El CSV es para una hoja de cálculo o para csvkit.&lt;/p&gt;

&lt;p&gt;Son cuatro versiones de un mismo documento, y el mismo grafo da siempre el mismo documento.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se obtiene
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover&lt;/code&gt; escribe el inventario junto con los diagramas. &lt;code&gt;vpcatlas inventory&lt;/code&gt; escanea igual pero no dibuja nada, que es lo que quieres para el anexo de una auditoría o para un job programado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--profile&lt;/span&gt; prod &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--output-dir&lt;/span&gt; out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--format&lt;/code&gt; acepta &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;md&lt;/code&gt;, &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt; o &lt;code&gt;all&lt;/code&gt;, repetido o separado por comas, y &lt;code&gt;all&lt;/code&gt; son los cuatro. Los ficheros se escriben junto a &lt;code&gt;vpcatlas-graph.json&lt;/code&gt; en el directorio de salida, &lt;code&gt;vpcatlas-out&lt;/code&gt; por defecto.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Finventory%2Finventory-csv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Finventory%2Finventory-csv.gif" alt="Grabación de vpcatlas inventory escribiendo el CSV contra la cuenta de AWS simulada y listando después las subnets de la región como tabla con csvkit" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La lista: la misma cuenta en CSV, y sus subnets filtradas con csvkit. Grabado contra la cuenta simulada, así que los identificadores son de ejemplo.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;El dashboard sirve los mismos cuatro en &lt;code&gt;/export/inventory.json&lt;/code&gt;, &lt;code&gt;/export/inventory.md&lt;/code&gt;, &lt;code&gt;/export/inventory.html&lt;/code&gt; y &lt;code&gt;/export/inventory.csv&lt;/code&gt;, y la pestaña de inventario tiene un botón &lt;code&gt;.csv&lt;/code&gt;. &lt;code&gt;/export/all.zip&lt;/code&gt; trae todo junto: los diagramas, el inventario en los cuatro formatos y &lt;code&gt;graph.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg809cuklwo2vheowk5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg809cuklwo2vheowk5v.png" alt="La pestaña de inventario del dashboard de vpcatlas: los recuentos del resumen, los recursos por tipo y los cuatro botones de exportación, .json, .md, .html y .csv" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La pestaña de inventario del dashboard, con los cuatro formatos a un clic.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Si ya tienes un grafo guardado, no necesitas AWS para nada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--cache&lt;/span&gt; ./out/vpcatlas-graph.json &lt;span class="nt"&gt;--format&lt;/span&gt; csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Tres maneras de filtrarlo
&lt;/h2&gt;

&lt;p&gt;En una hoja de cálculo, activa el filtro en la fila de cabecera y elige un valor en la columna &lt;code&gt;type&lt;/code&gt;, por ejemplo &lt;code&gt;subnet&lt;/code&gt;, y luego una región en la columna &lt;code&gt;region&lt;/code&gt;. Con eso tienes las subnets de una región.&lt;/p&gt;

&lt;p&gt;Con &lt;a href="https://csvkit.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;csvkit&lt;/a&gt;, la misma pregunta como comando, tal como está en la documentación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;csvgrep &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; subnet vpcatlas-out/vpcatlas-inventory.csv | csvcut &lt;span class="nt"&gt;-c&lt;/span&gt; region,vpc_id,id,name,cidr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con &lt;code&gt;jq&lt;/code&gt;, sobre el JSON, las subnets públicas, una por línea con cuenta, región, VPC, subnet, CIDR, zona y nivel. Es la receta de las referencias del agent skill con un &lt;code&gt;select&lt;/code&gt; añadido, y &lt;code&gt;INV&lt;/code&gt; es la ruta del fichero JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;INV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vpcatlas-out/vpcatlas-inventory.json
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.accounts[] as $a | $a.regions[] as $r | $r.vpcs[] as $v | $v.subnets[]
  | select(.tier == "public")
  | [$a.account_id, $r.region, $v.vpc.id, .subnet.id, .cidr, .availability_zone, .tier] | @tsv'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Por qué el CSV es como es
&lt;/h2&gt;

&lt;p&gt;La primera decisión fue la forma. El JSON es un árbol, y un árbol no cabe en una tabla. La respuesta ordenada parecía un fichero por nivel: cuentas, VPC, subnets, cada uno apuntando a su padre por id. No la construí, porque lo primero que se hace con una hoja de cálculo es filtrar, y un filtro trabaja sobre una sola hoja. Con tres ficheros, «todo lo que hay en esta subnet» se convierte en una búsqueda cruzada entre ficheros, que es justo el trabajo que quería ahorrarte.&lt;/p&gt;

&lt;p&gt;Así que es una sola tabla, con una fila por recurso y una columna &lt;code&gt;scope&lt;/code&gt; que dice dónde está la fila: &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;vpc&lt;/code&gt;, &lt;code&gt;subnet&lt;/code&gt; o &lt;code&gt;service&lt;/code&gt;. Los ids de la VPC y de la subnet se repiten en cada fila que cuelga de ellas, de modo que una fila sigue teniendo sentido cuando has filtrado todo lo demás. El precio son 23 columnas. Las filas van en el orden del documento, la fila de una subnet y luego los recursos que hay en ella. Para un recurso de la capa de servicios, &lt;code&gt;family&lt;/code&gt; dice a qué familia pertenece.&lt;/p&gt;

&lt;p&gt;Con los detalles y los tags pasa lo contrario. Una subnet y un NAT gateway no comparten atributos, así que una columna por atributo daría una tabla muy ancha y casi vacía. Los dos se escriben como &lt;code&gt;clave=valor; clave=valor&lt;/code&gt;, en el orden del documento, y a diferencia del Markdown los detalles no se cortan.&lt;/p&gt;

&lt;p&gt;La protección de las celdas salió de lo que hay en ellas. Los tags son texto que puede escribir cualquiera con acceso a la cuenta, y una hoja de cálculo ejecuta una celda que empieza por &lt;code&gt;=&lt;/code&gt;. Por eso una celda que empieza por &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; o &lt;code&gt;@&lt;/code&gt; recibe un apóstrofo delante, y la hoja enseña el texto en vez de ejecutarlo. También cuentan las formas de ancho completo de esos signos, y un tabulador o un retorno de carro delante de uno se quitan antes, para que no pueda esconderlo. La contrapartida es que una celda que empieza por un signo menos recibe también el apóstrofo, sea inofensiva o no. El apóstrofo forma parte del fichero. Quítalo si lees el CSV desde código.&lt;/p&gt;

&lt;p&gt;Lo demás es más pequeño. Las líneas terminan en CRLF, como define el formato CSV. La cabecera se escribe siempre, aunque el escaneo no encuentre ningún recurso, así que un script que lee el fichero por nombre de columna nunca se topa con un fichero vacío. Un valor que falta es una celda vacía, y el mismo documento da los mismos bytes, de modo que dos CSV del mismo grafo salen idénticos. El fichero es UTF-8 sin marca de orden de bytes: si una hoja de cálculo muestra mal los acentos, importa el fichero y elige UTF-8. Una hoja también puede leer un id de cuenta de 12 dígitos como número y quitarle los ceros del principio, así que importa esa columna como texto cuando necesites el id exacto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que no hace
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El CSV tiene los recursos y nada más. Las cuentas externas, los enlaces entre cuentas y las incidencias no son filas; están en el JSON y en el Markdown.&lt;/li&gt;
&lt;li&gt;El número de filas puede no coincidir con &lt;code&gt;summary.resources&lt;/code&gt; del JSON. Una VPC que se mencionó pero que el escaneo nunca describió tiene una fila de relleno en el árbol, y el resumen no la cuenta.&lt;/li&gt;
&lt;li&gt;Un escaneo solo puede nombrar lo que ven sus credenciales. VPC Atlas todavía no descubre las cuentas de una AWS Organization: las das tú, repitiendo &lt;code&gt;--profile&lt;/code&gt; o &lt;code&gt;--role-arn&lt;/code&gt;. Si se ejecuta desde la cuenta de administración, lee Organizations solo para ponerles nombre a las cuentas.&lt;/li&gt;
&lt;li&gt;Cada fichero es un escaneo, con la hora de ese escaneo en &lt;code&gt;generated_at&lt;/code&gt;. Es una instantánea, no un historial. Para comparar dos momentos guardas dos ficheros de grafo, o dejas que el dashboard registre los escaneos y dibuje la diferencia.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dime qué falta
&lt;/h2&gt;

&lt;p&gt;El formato está en la &lt;a href="https://vpcatlas.com/docs/outputs/inventory/" rel="noopener noreferrer"&gt;documentación del inventario&lt;/a&gt; y el comando, en la &lt;a href="https://vpcatlas.com/docs/cli/inventory/" rel="noopener noreferrer"&gt;página de &lt;code&gt;inventory&lt;/code&gt;&lt;/a&gt;. Si echaste en falta una columna, o acabaste escribiendo a mano un filtro, dime cuál.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>aws</category>
      <category>cloud</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>The AWS network inventory, now also a CSV you can filter</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 23:25:39 +0000</pubDate>
      <link>https://dev.to/karinapp/the-aws-network-inventory-now-also-a-csv-you-can-filter-41h4</link>
      <guid>https://dev.to/karinapp/the-aws-network-inventory-now-also-a-csv-you-can-filter-41h4</guid>
      <description>&lt;p&gt;What the VPC Atlas inventory holds, the four formats it comes in, and why I made the CSV one flat row per resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagram is the picture, the inventory is the list
&lt;/h2&gt;

&lt;p&gt;A diagram tells you what a network looks like. It's a poor answer to "which subnets are public in eu-west-1", to "what changed since last month" or to "send me every VPC with its CIDRs". You can't search a picture, and nobody diffs one by eye with any confidence. For those questions I wanted a list: one document with everything the scan found, that a person can filter and a script can read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fneprna8p24148a65md1b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fneprna8p24148a65md1b.png" alt="The diagram of the prod VPC of the simulated account in draw.io: nine subnets in three zones, route tables, NAT gateways and numbered flows" width="800" height="846"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The picture: the prod VPC of the simulated account, as VPC Atlas draws it. Each resource drawn here is one row in the inventory below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;VPC Atlas already wrote that document as JSON, Markdown and HTML. Version 0.7.0 adds a fourth rendering, a CSV. This post covers what the inventory holds, who each format is for, how to get it, three ways to filter it, and the choices I made in the CSV, including the one that puts a stray apostrophe in your cells.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the document holds
&lt;/h2&gt;

&lt;p&gt;The inventory is the account written as a tree. Under each audited account come its regions. Under a region come its VPCs, plus the resources that sit in no VPC, such as Transit Gateways and peering connections. A VPC holds its subnets and the resources that sit in no subnet, such as route tables and security groups. A subnet holds what's placed in it, such as NAT gateways and load balancers, and carries its Availability Zone, its CIDR and a tier: public when the default route goes to an internet gateway, otherwise private, isolated or unknown.&lt;/p&gt;

&lt;p&gt;Next to the network tree, each account has a service layer in five families: DNS and edge, API and load balancing, compute and containers, data and storage, and account services such as CloudTrail and GuardDuty. When the scan couldn't read a value in the account services, the file says &lt;code&gt;not read&lt;/code&gt;. That's different from zero or &lt;code&gt;no&lt;/code&gt;, so a denied call never reads as an empty account.&lt;/p&gt;

&lt;p&gt;Then come the parts that cross accounts. Accounts the scan saw but has no credentials for are listed as external accounts, every link that crosses an account boundary has its own entry, and the calls that failed while discovering are listed as issues. A summary with the counts opens the document. Every resource carries the same fields: id, name, ARN, state, its details, its tags, whether the owner is an account you didn't audit, and which audited accounts saw it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four formats, four readers
&lt;/h2&gt;

&lt;p&gt;JSON, in &lt;code&gt;vpcatlas-inventory.json&lt;/code&gt;, is for machines and for &lt;code&gt;jq&lt;/code&gt;. Its schema id is &lt;code&gt;vpcatlas-inventory/1&lt;/code&gt;. Markdown is for a pull request or a wiki: the same document as tables, with the details cut at 240 characters per cell. HTML is a single self-contained page with a search box that filters rows by id, name, type or account, and it's the one I'd send to someone who won't open a terminal. CSV is for a spreadsheet or csvkit.&lt;/p&gt;

&lt;p&gt;They're four renderings of one document, and the same graph always gives the same document.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vpcatlas discover&lt;/code&gt; writes the inventory with the diagrams. &lt;code&gt;vpcatlas inventory&lt;/code&gt; scans the same way but draws nothing, which is what you want for an audit attachment or a scheduled job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--profile&lt;/span&gt; prod &lt;span class="nt"&gt;--format&lt;/span&gt; json &lt;span class="nt"&gt;--output-dir&lt;/span&gt; out
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--format&lt;/code&gt; takes &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;md&lt;/code&gt;, &lt;code&gt;html&lt;/code&gt;, &lt;code&gt;csv&lt;/code&gt; or &lt;code&gt;all&lt;/code&gt;, repeated or separated with commas, and &lt;code&gt;all&lt;/code&gt; is the four. The files land next to &lt;code&gt;vpcatlas-graph.json&lt;/code&gt; in the output directory, &lt;code&gt;vpcatlas-out&lt;/code&gt; by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Finventory%2Finventory-csv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Finventory%2Finventory-csv.gif" alt="Recording of vpcatlas inventory writing the CSV against the simulated AWS account, then listing the subnets of the region as a table with csvkit" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The list: the same account as a CSV, and its subnets filtered with csvkit. Recorded against the simulated account, so the ids are examples.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard serves the same four at &lt;code&gt;/export/inventory.json&lt;/code&gt;, &lt;code&gt;/export/inventory.md&lt;/code&gt;, &lt;code&gt;/export/inventory.html&lt;/code&gt; and &lt;code&gt;/export/inventory.csv&lt;/code&gt;, and the inventory tab has a &lt;code&gt;.csv&lt;/code&gt; button. &lt;code&gt;/export/all.zip&lt;/code&gt; holds the lot: the diagrams, the inventory in the four formats, and &lt;code&gt;graph.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg809cuklwo2vheowk5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgg809cuklwo2vheowk5v.png" alt="The inventory tab of the vpcatlas dashboard: the summary counts, the resources by type and the four export buttons, .json, .md, .html and .csv" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The inventory tab of the dashboard, with the four formats one click away.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you already have a saved graph, you don't need AWS at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vpcatlas inventory &lt;span class="nt"&gt;--cache&lt;/span&gt; ./out/vpcatlas-graph.json &lt;span class="nt"&gt;--format&lt;/span&gt; csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three ways to filter it
&lt;/h2&gt;

&lt;p&gt;In a spreadsheet, turn on the filter on the header row and pick a value in the &lt;code&gt;type&lt;/code&gt; column, say &lt;code&gt;subnet&lt;/code&gt;, then a region in the &lt;code&gt;region&lt;/code&gt; column. That's all it takes to get the subnets of one region.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://csvkit.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;csvkit&lt;/a&gt;, the same question as a command, from the docs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;csvgrep &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; subnet vpcatlas-out/vpcatlas-inventory.csv | csvcut &lt;span class="nt"&gt;-c&lt;/span&gt; region,vpc_id,id,name,cidr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;jq&lt;/code&gt;, on the JSON, here are the public subnets, one per line with account, region, VPC, subnet, CIDR, zone and tier. This is the recipe from the agent skill's references with one &lt;code&gt;select&lt;/code&gt; added, and &lt;code&gt;INV&lt;/code&gt; is the path of the JSON file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;INV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;vpcatlas-out/vpcatlas-inventory.json
jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.accounts[] as $a | $a.regions[] as $r | $r.vpcs[] as $v | $v.subnets[]
  | select(.tier == "public")
  | [$a.account_id, $r.region, $v.vpc.id, .subnet.id, .cidr, .availability_zone, .tier] | @tsv'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INV&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why the CSV looks the way it does
&lt;/h2&gt;

&lt;p&gt;The first decision was the shape. The JSON is a tree, and a tree doesn't fit in a table. The tidy answer looked like one file per level: accounts, VPCs, subnets, each pointing at its parent by id. I didn't build that, because the first thing anyone does with a spreadsheet is filter, and a filter works on one sheet. With three files, "everything in this subnet" turns into a lookup between files, which is the work I wanted the file to save you.&lt;/p&gt;

&lt;p&gt;So it's one table with a row per resource and a &lt;code&gt;scope&lt;/code&gt; column that says where the row sits: &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;vpc&lt;/code&gt;, &lt;code&gt;subnet&lt;/code&gt; or &lt;code&gt;service&lt;/code&gt;. The VPC and subnet ids are repeated on every row under them, so a row still makes sense after you've filtered the rest away. The price is 23 columns. Rows come in document order, a subnet row and then the resources in it. For a resource of the service layer, &lt;code&gt;family&lt;/code&gt; says which one it belongs to.&lt;/p&gt;

&lt;p&gt;Details and tags are the opposite problem. A subnet and a NAT gateway don't share attributes, so a column for each would give a very wide table, mostly empty. Both are written as &lt;code&gt;key=value; key=value&lt;/code&gt;, in document order, and unlike the Markdown file the details aren't cut.&lt;/p&gt;

&lt;p&gt;The cell guard came from what's in the cells. Tags are text that anyone with access to the account can write, and a spreadsheet will run a cell that starts with &lt;code&gt;=&lt;/code&gt;. So a cell starting with &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt; or &lt;code&gt;@&lt;/code&gt; gets a leading apostrophe, which makes the spreadsheet show the text instead of running it. The full-width forms of those signs count too, and a tab or carriage return in front of one is removed first, so it can't hide the sign. The trade-off is that a cell starting with a minus sign gets the apostrophe as well, harmless or not. The apostrophe is part of the file. Strip it if you read the CSV in code.&lt;/p&gt;

&lt;p&gt;The rest is smaller. Lines end with CRLF, as the CSV format has it. The header row is always written, even when the scan found no resource, so a script that reads the file by column name never meets an empty file. A missing value is an empty cell, and the same document gives the same bytes, so two CSVs of the same graph compare equal. The file is UTF-8 with no byte order mark: if a spreadsheet shows accents wrongly, import the file and choose UTF-8. A spreadsheet may also read a 12-digit account id as a number and drop its leading zeros, so import that column as text when you need the exact id.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The CSV has the resources and nothing else. External accounts, cross-account links and issues aren't rows; they're in the JSON and the Markdown.&lt;/li&gt;
&lt;li&gt;The number of rows can differ from &lt;code&gt;summary.resources&lt;/code&gt; in the JSON. A VPC that was referenced but never described in the scan has a placeholder row in the tree, and the summary doesn't count it.&lt;/li&gt;
&lt;li&gt;A scan can only name what its credentials can see. VPC Atlas doesn't discover the accounts of an AWS Organization yet: you list them yourself, with &lt;code&gt;--profile&lt;/code&gt; or &lt;code&gt;--role-arn&lt;/code&gt; repeated. When it runs from the management account, it reads Organizations only to put names on accounts.&lt;/li&gt;
&lt;li&gt;Each file is one scan, with the time of that scan in &lt;code&gt;generated_at&lt;/code&gt;. It's a snapshot, not a history. To compare two moments you keep two graph files, or let the dashboard record the scans and draw the difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tell me what's missing
&lt;/h2&gt;

&lt;p&gt;The format reference is in the &lt;a href="https://vpcatlas.com/docs/outputs/inventory/" rel="noopener noreferrer"&gt;inventory docs&lt;/a&gt; and the command in the &lt;a href="https://vpcatlas.com/docs/cli/inventory/" rel="noopener noreferrer"&gt;&lt;code&gt;inventory&lt;/code&gt; page&lt;/a&gt;. If there's a column you needed and didn't find, or a filter you ended up writing by hand, tell me which one.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>Qué hace falta para fiarse del binario de una CLI de una sola persona</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 20:18:07 +0000</pubDate>
      <link>https://dev.to/karinapp/que-hace-falta-para-fiarse-del-binario-de-una-cli-de-una-sola-persona-58b8</link>
      <guid>https://dev.to/karinapp/que-hace-falta-para-fiarse-del-binario-de-una-cli-de-una-sola-persona-58b8</guid>
      <description>&lt;p&gt;Cómo construí el CI/CD y la cadena de suministro de VPC Atlas, y los comandos para comprobar cada descarga.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué una herramienta pequeña lleva tanto pipeline
&lt;/h2&gt;

&lt;p&gt;La herramienta solo hace llamadas de lectura, pero se ejecuta con credenciales de una cuenta de AWS, así que quien la instala debería saber qué está ejecutando.&lt;/p&gt;

&lt;p&gt;Quien hace merge y publica las releases soy yo, y nadie más. &lt;code&gt;CODEOWNERS&lt;/code&gt; tiene una sola regla y un solo nombre, el mío. Sin un segundo revisor que pille un error o un atajo, escribí las comprobaciones que haría un revisor y puse a una máquina a ejecutarlas en cada cambio, en público.&lt;/p&gt;

&lt;p&gt;Quería que cualquiera que descargue un fichero de la página de releases pudiera averiguar qué workflow lo construyó, y desde qué commit, sin fiarse de mi palabra. Lo que sigue es cada decisión y por qué la tomé. Los motivos sirven para otros proyectos. El YAML solo encaja en el mío.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo está montado el pipeline
&lt;/h2&gt;

&lt;p&gt;Hay cinco workflows. &lt;code&gt;ci&lt;/code&gt; se ejecuta en cada pull request y en cada push a &lt;code&gt;main&lt;/code&gt;. &lt;code&gt;release&lt;/code&gt; se ejecuta cuando subo un tag. CodeQL y OpenSSF Scorecard corren además cada lunes, y las comprobaciones nocturnas, a las 05:00 UTC, vigilan lo que puede romperse sin tocar el código: &lt;code&gt;pip-audit&lt;/code&gt; sobre los paquetes instalados, los enlaces de la documentación, un benchmark del layout y un análisis con Trivy de la imagen publicada. Cada job tiene un tiempo máximo. Solo un tag con la forma &lt;code&gt;vX.Y.Z&lt;/code&gt; dispara una publicación:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v[0-9]+.[0-9]+.[0-9]+"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué se ejecuta en cada cambio
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ci&lt;/code&gt; tiene nueve jobs y ninguno espera a otro, así que un cambio tarda lo que tarde el más lento. Un push nuevo a la misma rama cancela la ejecución anterior. Los nombres de los jobs son las comprobaciones obligatorias de &lt;code&gt;main&lt;/code&gt;, y no se aceptan commits directos a &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27io8npdf5fcbdtdjx4n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27io8npdf5fcbdtdjx4n.png" alt="La ejecución de ci sobre el commit que publicó la 0.7.0, en GitHub Actions: lint, comprobación de tipos, docs, paquete, imágenes, chart de Helm, revisión de dependencias y las matrices de tests e instaladores, dibujadas en una sola columna porque ninguno espera a otro" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Una ejecución de ci, sobre el commit que publicó la 0.7.0: nueve jobs y ninguna flecha entre ellos.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lint y tipos.&lt;/strong&gt; &lt;code&gt;ruff&lt;/code&gt; y &lt;code&gt;mypy&lt;/code&gt; sobre el Python, &lt;code&gt;actionlint&lt;/code&gt; y &lt;code&gt;zizmor&lt;/code&gt; sobre los ficheros de workflow. Cualquier hallazgo hace fallar el job. Los workflows pueden publicar mis releases, así que pasan las mismas comprobaciones que el Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests.&lt;/strong&gt; Linux con Python 3.12 y 3.13, macOS 14 y Windows 2022. Solo la combinación de Linux con Python 3.12 mide la cobertura, con &lt;code&gt;--cov-fail-under=88&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs y paquete.&lt;/strong&gt; &lt;code&gt;mkdocs build --strict&lt;/code&gt; falla ante un enlace roto, y el sdist y el wheel se construyen desde un lock con hashes fijados. La revisión de dependencias bloquea los pull requests que añaden una dependencia con un aviso de seguridad de severidad alta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imágenes.&lt;/strong&gt; Las imágenes del panel y de Lambda se construyen y se arrancan. El panel debe responder en &lt;code&gt;/healthz&lt;/code&gt;, y la imagen de Lambda debe responder a un evento de HTTP API enviado a través del Runtime Interface Emulator. Trivy hace fallar el job ante una vulnerabilidad crítica con corrección disponible. El tiempo máximo es de 40 minutos, el más alto de &lt;code&gt;ci&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chart de Helm.&lt;/strong&gt; &lt;code&gt;helm lint --strict&lt;/code&gt; y &lt;code&gt;helm template&lt;/code&gt; se ejecutan con los valores por defecto y con todas las opciones activadas. Después el job construye la imagen desde el commit, levanta un clúster &lt;code&gt;kind&lt;/code&gt;, instala el chart, ejecuta &lt;code&gt;helm test&lt;/code&gt;, llega al panel con un port-forward, actualiza con todas las opciones activadas y vuelve a probar. El tiempo máximo es de 30 minutos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instaladores.&lt;/strong&gt; En &lt;code&gt;windows-2022&lt;/code&gt;, &lt;code&gt;ubuntu-22.04&lt;/code&gt; y &lt;code&gt;macos-14&lt;/code&gt;, el job construye el archivo comprimido, pasa el script de instalación al intérprete como lo hace &lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt; y ejecuta &lt;code&gt;vpcatlas --version&lt;/code&gt;. Después le da al instalador una suma de verificación que no coincide y exige que falle con &lt;code&gt;checksum mismatch&lt;/code&gt;, sin dejar nada instalado. Una comprobación que nunca se ha visto fallar demuestra poco.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace un tag
&lt;/h2&gt;

&lt;p&gt;Para publicar, pongo la misma versión en &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;src/vpcatlas/__init__.py&lt;/code&gt; y &lt;code&gt;deploy/helm/vpcatlas/Chart.yaml&lt;/code&gt;, incorporo el cambio con un pull request, pongo el tag al commit de merge y lo subo. El primer job se niega a seguir si el tag y los cuatro campos de versión no coinciden:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;chart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^version: *\(.*\)$/\1/p'&lt;/span&gt; deploy/helm/vpcatlas/Chart.yaml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^appVersion: *"\(.*\)"$/\1/p'&lt;/span&gt; deploy/helm/vpcatlas/Chart.yaml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$chart&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pyproject.toml says &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; but Chart.yaml says version &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;chart&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and appVersion &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_TYPE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tag"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_NAME&lt;/span&gt;&lt;span class="p"&gt;#v&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tag &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not match the version &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después, por orden:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;binaries&lt;/strong&gt; se construyen con PyInstaller para Linux, macOS (Intel y Apple silicon) y Windows. Cada binario se ejecuta, y el que sale del archivo desempaquetado debe imprimir &lt;code&gt;--version&lt;/code&gt; en menos de tres segundos. La compilación de Linux x86_64 escribe además el SBOM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;release&lt;/strong&gt; escribe &lt;code&gt;SHA256SUMS&lt;/code&gt;, lo comprueba con &lt;code&gt;sha256sum -c&lt;/code&gt;, genera la atestación de cada archivo, del fichero de sumas y del SBOM, y crea la release de GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docker&lt;/strong&gt; y &lt;strong&gt;lambda&lt;/strong&gt; construyen cada imagen (la del panel, para &lt;code&gt;linux/amd64&lt;/code&gt; y &lt;code&gt;linux/arm64&lt;/code&gt;), la analizan con Trivy y se detienen antes del push si hay una vulnerabilidad crítica con corrección. Luego la suben, la firman por digest con cosign y generan su atestación.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chart&lt;/strong&gt; sube el chart de Helm a &lt;code&gt;ghcr.io/karina-pp/charts/vpcatlas&lt;/code&gt;, lo firma, genera su atestación y lo descarga de nuevo para verificar ambas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pypi&lt;/strong&gt; publica con trusted publishing: PyPI confía en la ejecución del workflow, así que no existe ningún token. Una variable del repositorio activa el job, porque antes hay que registrar el publicador en pypi.org.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;verify&lt;/strong&gt; va el último. Todos los jobs anteriores prueban mis ficheros. Este prueba lo que recibe el usuario.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ejecuta &lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt; en Linux y macOS y &lt;code&gt;irm https://vpcatlas.com/install.ps1 | iex&lt;/code&gt; en Windows, con la versión fijada, desde el sitio publicado y no desde el commit. &lt;code&gt;vpcatlas --version&lt;/code&gt; debe imprimir exactamente &lt;code&gt;vpcatlas X.Y.Z&lt;/code&gt;. En Linux verifica además la atestación y la firma de la imagen publicada. Si el sitio, el script, los ficheros de la release o las firmas están mal, este job falla.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd5rtd93t5anumoqmm4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd5rtd93t5anumoqmm4x.png" alt="La ejecución de release de la v0.7.0 en GitHub Actions: lint y tests, la matriz de binarios y el paquete de Python, después la GitHub Release, después la imagen de Docker Hub, las imágenes de Lambda, PyPI y el chart de Helm, y la matriz de verify al final" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La ejecución de release de la 0.7.0, tal como la dibuja GitHub: la release en el centro, las imágenes, PyPI y el chart después, y verify al final.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Si lanzo el workflow a mano desde la pestaña Actions, prueba y construye pero no publica nada, porque todos los jobs que publican llevan una condición sobre el tag, así que una ejecución manual es el ensayo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisiones que mantienen pequeña la cadena de suministro
&lt;/h2&gt;

&lt;p&gt;Todo lo que traigo de otros repositorios va fijado por contenido. Los workflows usan 25 actions de otros repositorios, cada una fijada a un SHA de commit completo con la versión en un comentario, y la única action de contenedor, por su digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1&lt;/span&gt; &lt;span class="c1"&gt;# v7.0.1&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;persist-credentials&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;Las imágenes base se fijan por digest y los requisitos de Python por hash, y se instalan con &lt;code&gt;--require-hashes&lt;/code&gt;. Un tag se puede mover después. Un SHA de commit, no. Dependabot propone las actualizaciones cada lunes, con una espera de siete días, así que una versión nueva tarda una semana en llegarme.&lt;/p&gt;

&lt;p&gt;Los cinco workflows empiezan con &lt;code&gt;permissions: contents: read&lt;/code&gt;. Un job pide más solo para lo que hace, como &lt;code&gt;id-token: write&lt;/code&gt; para firmar. Los 21 checkouts llevan &lt;code&gt;persist-credentials: false&lt;/code&gt;, así que el token no se queda en la copia de trabajo.&lt;/p&gt;

&lt;p&gt;Las firmas y las atestaciones usan la identidad efímera que GitHub da a cada ejecución del workflow, y PyPI usa trusted publishing, así que no hay ningún secreto de firma que se pueda filtrar. La única credencial de larga duración que queda es el token de Docker Hub, porque Docker Hub no acepta la identidad de GitHub.&lt;/p&gt;

&lt;p&gt;El workflow de release no restaura ninguna caché de compilación, ni siquiera la de Trivy, porque una caché escrita por una ejecución nunca debe acabar dentro de un fichero publicado.&lt;/p&gt;

&lt;p&gt;Hay un solo SBOM, un fichero CycloneDX escrito por Syft, que lista lo que hay dentro de la compilación de Linux x86_64 y lleva su atestación vinculada a ese archivo. Los demás se construyen con el mismo lock de dependencias, así que un fichero vale para todos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffyytxojm65zg27htu3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffyytxojm65zg27htu3a.png" alt="Los ficheros de la release 0.7.0 en GitHub: SHA256SUMS, los cinco archivos comprimidos, el SBOM y el chart de Helm, cada uno con su digest" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Lo que lleva una release: los archivos, SHA256SUMS, el SBOM y el chart, con el digest de cada fichero al lado.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Después de una release, CodeQL con las consultas &lt;code&gt;security-extended&lt;/code&gt;, Scorecard, &lt;code&gt;pip-audit&lt;/code&gt; y un análisis nocturno con Trivy de la imagen de Docker Hub siguen vigilando lo que ya está publicado. Si una noche falla, se abre un único issue con la etiqueta &lt;code&gt;nightly&lt;/code&gt; y los fallos siguientes se añaden como comentarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo comprobar una descarga
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SHA256SUMS&lt;/code&gt; viaja junto al archivo, así que solo demuestra que el fichero no se dañó por el camino. La atestación demuestra que lo construyó el workflow de release de este repositorio, y esa es la comprobación que importa. Necesitas la &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;CLI de GitHub&lt;/a&gt; y, para las imágenes, &lt;a href="https://docs.sigstore.dev/" rel="noopener noreferrer"&gt;cosign&lt;/a&gt; 3 o superior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh attestation verify vpcatlas-X.Y.Z-linux-x86_64.tar.gz &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
gh attestation verify SHA256SUMS &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt; SHA256SUMS       &lt;span class="c"&gt;# macOS: shasum -a 256 --check --ignore-missing SHA256SUMS&lt;/span&gt;
gh attestation verify oci://index.docker.io/vpcatlas/vpcatlas:X.Y.Z &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
cosign verify index.docker.io/vpcatlas/vpcatlas:X.Y.Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity-regexp&lt;/span&gt; &lt;span class="s1"&gt;'^https://github.com/karina-pp/vpcatlas/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; https://token.actions.githubusercontent.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una ejecución correcta nombra el workflow y el commit que produjeron el fichero. Si falla, no instales el fichero. Los scripts de instalación hacen por ti la comprobación del archivo cuando &lt;code&gt;gh&lt;/code&gt; está instalado y con la sesión iniciada, y &lt;code&gt;VPCATLAS_SKIP_ATTESTATION=1&lt;/code&gt; la desactiva.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Fcicd%2Fverify-download.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Fcicd%2Fverify-download.gif" alt="Grabación de las comprobaciones sobre un archivo descargado de la 0.7.0: la atestación del archivo y de SHA256SUMS, la suma de comprobación y la atestación de la imagen publicada, cada una nombrando el workflow de release" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Las comprobaciones sobre una descarga real de la 0.7.0, con la CLI de GitHub: el archivo, SHA256SUMS y la imagen publicada apuntan todos al workflow de release del tag.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué cuesta y qué me saltaría
&lt;/h2&gt;

&lt;p&gt;El primer coste es el tamaño: los cinco ficheros de workflow suman unas 1.650 líneas de YAML. El mantenimiento es el segundo: un lote semanal de pull requests de Dependabot y runners que cambian bajo tus pies. GitHub retiró el runner de macOS 13 con Intel, así que el workflow de release construye ahora ese binario en &lt;code&gt;macos-15-intel&lt;/code&gt;. Las herramientas también discrepan: las líneas que usan la action local de setup llevan un comentario para ignorar un aviso, porque zizmor y &lt;code&gt;actionlint&lt;/code&gt; 1.7.12 no pueden quedar contentos a la vez. El tercer coste es la espera. En &lt;code&gt;ci&lt;/code&gt;, los tiempos máximos van de 10 minutos para la revisión de dependencias a 40 para las imágenes.&lt;/p&gt;

&lt;p&gt;Para un proyecto más pequeño, con un solo binario y sin chart, sin imagen de Lambda ni script de instalación, me saltaría el clúster &lt;code&gt;kind&lt;/code&gt;, la prueba con el emulador, la matriz de instaladores, el benchmark nocturno del layout y Scorecard. Scorecard puntúa el repositorio, y lo que protege son las versiones fijadas y los permisos que puntúa. Conservaría lo que casi no cuesta: SHA fijados, &lt;code&gt;contents: read&lt;/code&gt; por defecto, &lt;code&gt;zizmor&lt;/code&gt;, Dependabot, &lt;code&gt;SHA256SUMS&lt;/code&gt; con una atestación de procedencia de la compilación y un último job que instala la release publicada como lo haría un usuario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que aún falta
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin certificado de firma de código.&lt;/strong&gt; La atestación dice quién construyó un fichero, pero Windows y macOS no la leen, así que SmartScreen y Gatekeeper pueden seguir avisando ante un fichero descargado con el navegador.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La verificación depende de &lt;code&gt;gh&lt;/code&gt;.&lt;/strong&gt; Sin la CLI de GitHub con la sesión iniciada, los instaladores solo comprueban la suma de verificación, que demuestra que el fichero llegó entero, no quién lo hizo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Las firmas llegan tarde.&lt;/strong&gt; Las imágenes firmadas y los SBOM existen solo en las releases hechas con este pipeline. Las anteriores solo llevan las atestaciones de los archivos y &lt;code&gt;SHA256SUMS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cinco archivos, a veces cuatro.&lt;/strong&gt; Linux arm64 se construye solo mientras hay una variable del repositorio activa, así que una release puede llevar cuatro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Actualización, 23 de octubre de 2026:&lt;/em&gt; el repositorio del código es privado por ahora, así que los pasos que necesitan un repositorio público están en pausa: las atestaciones, CodeQL, la revisión de dependencias y el último job que instala la release con los instaladores de la web. Las releases se copian a &lt;code&gt;vpcatlas.com/releases&lt;/code&gt;, los instaladores descargan de ahí (ver &lt;a href="https://vpcatlas.com/docs/install/" rel="noopener noreferrer"&gt;Install&lt;/a&gt;) y una descarga se comprueba con &lt;code&gt;SHA256SUMS&lt;/code&gt; (las imágenes y el chart, con cosign).&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde mirar
&lt;/h2&gt;

&lt;p&gt;Los workflows corren en GitHub Actions en el repositorio del código, que es privado por ahora, y la lista completa de comprobaciones está en la &lt;a href="https://vpcatlas.com/docs/engineering/" rel="noopener noreferrer"&gt;página de ingeniería&lt;/a&gt; de la documentación. Lo que dibuja la herramienta en sí está en &lt;a href="https://vpcatlas.com/es/blog/nadie-mantiene-al-dia-el-diagrama-de-red-de-aws/" rel="noopener noreferrer"&gt;el post de lanzamiento&lt;/a&gt;. El paquete está en &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; y la imagen en &lt;a href="https://hub.docker.com/r/vpcatlas/vpcatlas" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;. Si algún paso no comprueba lo que dice su nombre, dímelo.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>What a one-person CLI needs before you can trust its binary</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 20:17:08 +0000</pubDate>
      <link>https://dev.to/karinapp/what-a-one-person-cli-needs-before-you-can-trust-its-binary-5f71</link>
      <guid>https://dev.to/karinapp/what-a-one-person-cli-needs-before-you-can-trust-its-binary-5f71</guid>
      <description>&lt;p&gt;How I built the CI/CD and supply chain of VPC Atlas, and the commands that let you check every download.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a small tool gets this much pipeline
&lt;/h2&gt;

&lt;p&gt;The tool only makes read-only calls, but it runs with credentials for an AWS account, so whoever installs it should know what they are running.&lt;/p&gt;

&lt;p&gt;Only the maintainer merges and releases, and that's me. &lt;code&gt;CODEOWNERS&lt;/code&gt; has one rule with one name. With no second reviewer to catch a mistake or a shortcut, I wrote down the checks a reviewer would make and had a machine run them on every change, in public.&lt;/p&gt;

&lt;p&gt;I wanted anyone who downloads a file from the releases page to be able to find out which workflow built it, and from which commit, without taking my word for it. What follows is each decision and why I made it. The reasons carry over to other projects. The YAML only fits mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the pipeline
&lt;/h2&gt;

&lt;p&gt;There are five workflows. &lt;code&gt;ci&lt;/code&gt; runs on every pull request and every push to &lt;code&gt;main&lt;/code&gt;. &lt;code&gt;release&lt;/code&gt; runs when I push a tag. CodeQL and OpenSSF Scorecard also run every Monday, and nightly checks run at 05:00 UTC for what can break without a code change: &lt;code&gt;pip-audit&lt;/code&gt; on the installed packages, the links in the docs, a layout benchmark and a Trivy scan of the published image. Every job has a timeout. Only a tag of the form &lt;code&gt;vX.Y.Z&lt;/code&gt; publishes:&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;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v[0-9]+.[0-9]+.[0-9]+"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What runs on every change
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ci&lt;/code&gt; has nine jobs and none waits for another, so a change takes as long as its slowest job. A newer push to the same branch cancels the older run. The job names are the required checks of &lt;code&gt;main&lt;/code&gt;, and direct commits to &lt;code&gt;main&lt;/code&gt; are not accepted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27io8npdf5fcbdtdjx4n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27io8npdf5fcbdtdjx4n.png" alt="The ci run on the commit that released 0.7.0, in GitHub Actions: lint, type check, docs, package, images, Helm chart, dependency review, and the test and installer matrices, drawn in one column because none waits for another" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One ci run, on the commit that released 0.7.0: nine jobs and no arrows between them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lint and types.&lt;/strong&gt; &lt;code&gt;ruff&lt;/code&gt; and &lt;code&gt;mypy&lt;/code&gt; on the Python, &lt;code&gt;actionlint&lt;/code&gt; and &lt;code&gt;zizmor&lt;/code&gt; on the workflow files. Any finding fails the job. The workflows can publish my releases, so they get the same checks as the Python.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests.&lt;/strong&gt; Linux on Python 3.12 and 3.13, macOS 14 and Windows 2022. Only the Linux 3.12 leg measures coverage, with &lt;code&gt;--cov-fail-under=88&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docs and package.&lt;/strong&gt; &lt;code&gt;mkdocs build --strict&lt;/code&gt; fails on a broken link, and the sdist and wheel are built from a hash-pinned lock. Dependency review blocks pull requests that add a dependency with a high-severity advisory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Images.&lt;/strong&gt; The dashboard and Lambda images are built and started. The dashboard must answer &lt;code&gt;/healthz&lt;/code&gt;, and the Lambda image must answer an HTTP API event sent through the Runtime Interface Emulator. Trivy fails the job on a fixable critical vulnerability. The timeout is 40 minutes, the highest in &lt;code&gt;ci&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Helm chart.&lt;/strong&gt; &lt;code&gt;helm lint --strict&lt;/code&gt; and &lt;code&gt;helm template&lt;/code&gt; run with the defaults and with every option on. Then the job builds the image from the commit, starts a &lt;code&gt;kind&lt;/code&gt; cluster, installs the chart, runs &lt;code&gt;helm test&lt;/code&gt;, reaches the dashboard through a port-forward, upgrades with every option on and tests again. The timeout is 30 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installers.&lt;/strong&gt; On &lt;code&gt;windows-2022&lt;/code&gt;, &lt;code&gt;ubuntu-22.04&lt;/code&gt; and &lt;code&gt;macos-14&lt;/code&gt;, the job builds the archive, pipes the install script into the shell the way &lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt; does, and runs &lt;code&gt;vpcatlas --version&lt;/code&gt;. Then it hands the installer a checksum that does not match and requires a &lt;code&gt;checksum mismatch&lt;/code&gt; failure, with nothing left behind. A check that has never been seen to fail proves little.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a tag does
&lt;/h2&gt;

&lt;p&gt;To release, I set the same version in &lt;code&gt;pyproject.toml&lt;/code&gt;, &lt;code&gt;src/vpcatlas/__init__.py&lt;/code&gt; and &lt;code&gt;deploy/helm/vpcatlas/Chart.yaml&lt;/code&gt;, merge that through a pull request, tag the merge commit and push the tag. The first job refuses to go on unless the tag and all four version fields agree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;chart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^version: *\(.*\)$/\1/p'&lt;/span&gt; deploy/helm/vpcatlas/Chart.yaml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s1"&gt;'s/^appVersion: *"\(.*\)"$/\1/p'&lt;/span&gt; deploy/helm/vpcatlas/Chart.yaml&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$chart&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"pyproject.toml says &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; but Chart.yaml says version &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;chart&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and appVersion &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_TYPE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tag"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_NAME&lt;/span&gt;&lt;span class="p"&gt;#v&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$package&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tag &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GITHUB_REF_NAME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not match the version &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;package&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;binaries&lt;/strong&gt; are built with PyInstaller for Linux, macOS on Intel and Apple silicon, and Windows. Each build is run, and the binary from the unpacked archive must print &lt;code&gt;--version&lt;/code&gt; in under three seconds. The Linux x86_64 build also writes the SBOM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;release&lt;/strong&gt; writes &lt;code&gt;SHA256SUMS&lt;/code&gt;, checks it with &lt;code&gt;sha256sum -c&lt;/code&gt;, attests every archive, the checksum file and the SBOM, and creates the GitHub Release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;docker&lt;/strong&gt; and &lt;strong&gt;lambda&lt;/strong&gt; build each image (the dashboard one for &lt;code&gt;linux/amd64&lt;/code&gt; and &lt;code&gt;linux/arm64&lt;/code&gt;), scan it with Trivy, and stop before the push on a fixable critical vulnerability. Then they push, sign by digest with cosign and attest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chart&lt;/strong&gt; pushes the Helm chart to &lt;code&gt;ghcr.io/karina-pp/charts/vpcatlas&lt;/code&gt;, signs and attests it, and pulls it back to verify both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pypi&lt;/strong&gt; publishes through trusted publishing: PyPI trusts the workflow run, so no token exists. A repository variable switches the job on, because the publisher must be registered on pypi.org first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;verify&lt;/strong&gt; runs last. Every job before it tests my files. This one tests what users get.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It runs &lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt; on Linux and macOS and &lt;code&gt;irm https://vpcatlas.com/install.ps1 | iex&lt;/code&gt; on Windows, with the version pinned, from the live site and not from the commit. &lt;code&gt;vpcatlas --version&lt;/code&gt; must print exactly &lt;code&gt;vpcatlas X.Y.Z&lt;/code&gt;. On Linux it also verifies the attestation and signature of the published image. If the site, the script, the assets or the signatures are wrong, this job fails.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd5rtd93t5anumoqmm4x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwd5rtd93t5anumoqmm4x.png" alt="The release run of v0.7.0 in GitHub Actions: lint and test, the binaries matrix and the Python package, then the GitHub Release, then the Docker Hub image, the Lambda images, PyPI and the Helm chart, and the verify matrix last" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The release run of 0.7.0, as GitHub draws it: the release in the middle, the images, PyPI and the chart after it, and verify last.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Started by hand from the Actions tab, the workflow tests and builds but publishes nothing, because every publishing job has a condition on the tag, so a manual run is the rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions that keep the supply chain small
&lt;/h2&gt;

&lt;p&gt;Everything I pull from other repositories is pinned by content. The workflows use 25 actions from other repositories, each pinned to a full commit SHA with the version in a comment, and the one container action by its digest:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1&lt;/span&gt; &lt;span class="c1"&gt;# v7.0.1&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;persist-credentials&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;Base images are pinned by digest and Python requirements by hash, installed with &lt;code&gt;--require-hashes&lt;/code&gt;. A tag can be moved after the fact. A commit SHA cannot. Dependabot proposes the bumps every Monday, with a seven-day cooldown, so a new release waits a week before it reaches me.&lt;/p&gt;

&lt;p&gt;All five workflows start with &lt;code&gt;permissions: contents: read&lt;/code&gt;. A job asks for more only for what it does, such as &lt;code&gt;id-token: write&lt;/code&gt; to sign. All 21 checkouts set &lt;code&gt;persist-credentials: false&lt;/code&gt;, so the token does not stay in the working copy.&lt;/p&gt;

&lt;p&gt;Signatures and attestations use the short-lived identity GitHub gives each workflow run, and PyPI uses trusted publishing, so there is no signing secret to leak. The one long-lived credential left is the Docker Hub token, because Docker Hub does not accept GitHub's identity.&lt;/p&gt;

&lt;p&gt;The release workflow restores no build cache, not even Trivy's, because a cache written by one run must never end up inside a published file.&lt;/p&gt;

&lt;p&gt;There is one SBOM, a CycloneDX file written by Syft, that lists what is inside the Linux x86_64 build and is attested against that archive. The other archives are built from the same lock of dependencies, so one file stands for all of them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffyytxojm65zg27htu3a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffyytxojm65zg27htu3a.png" alt="The assets of the 0.7.0 release on GitHub: SHA256SUMS, the five archives, the SBOM and the Helm chart, each with its digest" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What a release carries: the archives, SHA256SUMS, the SBOM and the chart, with the digest of each file next to it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After a release, CodeQL with the &lt;code&gt;security-extended&lt;/code&gt; queries, Scorecard, &lt;code&gt;pip-audit&lt;/code&gt; and a nightly Trivy scan of the image on Docker Hub keep watching what is already published. A failing night opens one issue labelled &lt;code&gt;nightly&lt;/code&gt;, and later failures comment on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check a download
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SHA256SUMS&lt;/code&gt; travels with the archive, so it only proves the file was not damaged on the way. The attestation proves the archive was built by this repository's release workflow, and that is the check that matters. You need the &lt;a href="https://cli.github.com/" rel="noopener noreferrer"&gt;GitHub CLI&lt;/a&gt; and, for images, &lt;a href="https://docs.sigstore.dev/" rel="noopener noreferrer"&gt;cosign&lt;/a&gt; 3 or newer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh attestation verify vpcatlas-X.Y.Z-linux-x86_64.tar.gz &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
gh attestation verify SHA256SUMS &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt; &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt; SHA256SUMS       &lt;span class="c"&gt;# macOS: shasum -a 256 --check --ignore-missing SHA256SUMS&lt;/span&gt;
gh attestation verify oci://index.docker.io/vpcatlas/vpcatlas:X.Y.Z &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
cosign verify index.docker.io/vpcatlas/vpcatlas:X.Y.Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-identity-regexp&lt;/span&gt; &lt;span class="s1"&gt;'^https://github.com/karina-pp/vpcatlas/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--certificate-oidc-issuer&lt;/span&gt; https://token.actions.githubusercontent.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful run names the workflow and the commit that produced the file. If it fails, do not install the file. The install scripts run the archive check for you when &lt;code&gt;gh&lt;/code&gt; is installed and signed in, and &lt;code&gt;VPCATLAS_SKIP_ATTESTATION=1&lt;/code&gt; turns it off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Fcicd%2Fverify-download.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fvpcatlas.com%2Fassets%2Fblog%2Fcicd%2Fverify-download.gif" alt="Recording of the checks on a downloaded 0.7.0 archive: the attestation of the archive and of SHA256SUMS, the checksum, and the attestation of the published image, each naming the release workflow" width="799" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The checks on a real download of 0.7.0, with the GitHub CLI: the archive, SHA256SUMS and the published image all point at the release workflow of the tag.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs, and what I would skip
&lt;/h2&gt;

&lt;p&gt;The first cost is size: the five workflow files add up to about 1,650 lines of YAML. Upkeep is the second: a weekly batch of Dependabot pull requests, and runners that change under you. GitHub retired the Intel macOS 13 runner, so the release workflow now builds that binary on &lt;code&gt;macos-15-intel&lt;/code&gt;. Tools disagree too: the lines that use the local setup action carry an ignore comment because zizmor and &lt;code&gt;actionlint&lt;/code&gt; 1.7.12 cannot both be pleased. The third cost is the wait. In &lt;code&gt;ci&lt;/code&gt;, the timeouts run from 10 minutes for the dependency review to 40 for the images.&lt;/p&gt;

&lt;p&gt;For a smaller project, with one binary and no chart, Lambda image or installer script, I'd skip the &lt;code&gt;kind&lt;/code&gt; cluster, the emulator test, the installer matrix, the nightly layout benchmark and Scorecard. Scorecard grades the repository, and the pins and permissions it grades do the protecting. I'd keep what costs almost nothing: pinned SHAs, &lt;code&gt;contents: read&lt;/code&gt; by default, &lt;code&gt;zizmor&lt;/code&gt;, Dependabot, &lt;code&gt;SHA256SUMS&lt;/code&gt; with a build provenance attestation, and a last job that installs the published release the way a user does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still missing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No code-signing certificate.&lt;/strong&gt; The attestation tells you who built a file, but Windows and macOS do not read it, so SmartScreen and Gatekeeper can still warn about a file downloaded with a browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification depends on &lt;code&gt;gh&lt;/code&gt;.&lt;/strong&gt; Without &lt;code&gt;gh&lt;/code&gt; installed and signed in, the installers only check the checksum, which proves the file arrived intact, not who made it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signatures start late.&lt;/strong&gt; Signed images and SBOMs exist only for releases made by this pipeline. Earlier ones carry only the attestations of the archives and &lt;code&gt;SHA256SUMS&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Five archives, sometimes four.&lt;/strong&gt; Linux arm64 is built only while a repository variable is set, so a release can carry four.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Update, 23 October 2026:&lt;/em&gt; the source repository is private for now, so the steps that need a public repository are paused: the artifact attestations, CodeQL, the dependency review and the last job that installs the release with the live installers. Releases are mirrored to &lt;code&gt;vpcatlas.com/releases&lt;/code&gt;, the installers download from there (see &lt;a href="https://vpcatlas.com/docs/install/" rel="noopener noreferrer"&gt;Install&lt;/a&gt;), and a download is checked against &lt;code&gt;SHA256SUMS&lt;/code&gt; (the images and the chart with cosign).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to look
&lt;/h2&gt;

&lt;p&gt;The workflows run on GitHub Actions in the source repository, which is private for now, and the full list of checks is on the &lt;a href="https://vpcatlas.com/docs/engineering/" rel="noopener noreferrer"&gt;engineering page&lt;/a&gt; of the docs. What the tool itself draws is in the &lt;a href="https://vpcatlas.com/blog/nobody-keeps-the-aws-network-diagram-current/" rel="noopener noreferrer"&gt;launch post&lt;/a&gt;. The package is on &lt;a href="https://pypi.org/project/vpcatlas" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; and the image on &lt;a href="https://hub.docker.com/r/vpcatlas/vpcatlas" rel="noopener noreferrer"&gt;Docker Hub&lt;/a&gt;. If a step does not check what its name says, tell me.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>cli</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Nadie mantiene al día el diagrama de red de AWS. He hecho una herramienta para eso</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:10:15 +0000</pubDate>
      <link>https://dev.to/karinapp/nadie-mantiene-al-dia-el-diagrama-de-red-de-aws-he-hecho-una-herramienta-para-eso-51gl</link>
      <guid>https://dev.to/karinapp/nadie-mantiene-al-dia-el-diagrama-de-red-de-aws-he-hecho-una-herramienta-para-eso-51gl</guid>
      <description>&lt;p&gt;VPC Atlas lee una cuenta de AWS con llamadas de solo lectura y dibuja su red en draw.io y Lucidchart. En alfa, con licencia MIT.&lt;/p&gt;

&lt;p&gt;En todos los equipos a los que me he incorporado, lo primero que pedían era lo mismo: el diagrama de red. El que muestra cada VPC, cómo se conectan las VPC entre sí a través de Transit Gateways, peerings, VPN y Direct Connect, y qué hay en el borde, delante de ellas. Y siempre, el diagrama que existía estaba desactualizado.&lt;/p&gt;

&lt;h2&gt;
  
  
  A mano, o desde la consola
&lt;/h2&gt;

&lt;p&gt;Un diagrama de red dibujado a mano está mal una semana después. Alguien añade un attachment, alguien comparte un Transit Gateway por AWS RAM, aparece un peering con otra cuenta, y el dibujo se queda como estaba.&lt;/p&gt;

&lt;p&gt;La alternativa es reconstruirlo desde la consola: abrir cada VPC, leer las tablas de rutas, seguir los attachments, revisar los endpoints y, después, dibujarlo. En una cuenta real eso lleva días. Yo quería apuntar una herramienta a la cuenta, tener el diagrama en minutos y repetirlo la semana siguiente para comparar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué hace VPC Atlas
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vpcatlas&lt;/code&gt; se conecta a una cuenta de AWS con permisos de auditor y mapea su red: VPC, subredes, enrutamiento, gateways, endpoints, balanceadores, firewalls, el resolver de DNS y lo que se comparte por RAM, más lo que hay delante: Route 53, CloudFront, WAF, ACM y Shield. También lee el cómputo y las bases de datos y el almacenamiento, como RDS, S3 o DynamoDB, para que cada subred muestre lo que corre en ella, y servicios de la cuenta como CloudTrail, GuardDuty o KMS para el inventario.&lt;/p&gt;

&lt;p&gt;Es de solo lectura por diseño. Llama a las API &lt;code&gt;Describe*&lt;/code&gt;, &lt;code&gt;Get*&lt;/code&gt;, &lt;code&gt;List*&lt;/code&gt; y &lt;code&gt;Search*&lt;/code&gt; y nunca crea, cambia ni borra nada. Trae la política mínima que necesita, así que puedes leer lo que pide antes de concedérselo.&lt;/p&gt;

&lt;p&gt;Una ejecución te da tres cosas. Los diagramas: una página global, una página por región y una página por VPC, como ficheros &lt;code&gt;.drawio&lt;/code&gt; y &lt;code&gt;.lucid&lt;/code&gt;, dibujados con los AWS Architecture Icons y con el layout de la referencia de red multicuenta (landing zone) de AWS Managed Services. Un inventario en JSON, Markdown y HTML, con cada recurso cuenta por cuenta y una sección por cada cuenta externa vinculada a la tuya. Y un panel: &lt;code&gt;vpcatlas serve&lt;/code&gt; muestra los mismos diagramas en el navegador, guarda un historial de escaneos y dibuja la diferencia entre dos de ellos, lo añadido en verde y lo que cambió en naranja. El resultado en bruto se guarda también, como &lt;code&gt;vpcatlas-graph.json&lt;/code&gt;, para que puedas volver a dibujar más tarde sin llamar a AWS otra vez.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" alt="Grabación de vpcatlas discover ejecutándose contra una cuenta de AWS simulada y listando después los ficheros de diagrama generados" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Las grabaciones y los diagramas de este artículo salen de una cuenta de AWS simulada, así que los identificadores son de ejemplo. Este es el panel sobre esa cuenta:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphd6cwgyzijat2sa463q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphd6cwgyzijat2sa463q.png" alt="El panel de vpcatlas con los diagramas, el historial de ejecuciones y las descargas" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Y una página de región:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8roh4u579a7of4jzspe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8roh4u579a7of4jzspe.png" alt="El diagrama de la región eu-west-1 en draw.io: VPC, Transit Gateway, endpoints y gateways" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo funciona
&lt;/h2&gt;

&lt;p&gt;Un pipeline de solo lectura con cuatro etapas: recolección, grafo, layout y exportación. Cada etapa recibe una entrada y devuelve una salida, y eso es lo que la hace fácil de probar.&lt;/p&gt;

&lt;p&gt;Los colectores leen la cuenta, uno por servicio, todos paginados. Las regiones van en paralelo, pero dentro de cada región los colectores van uno detrás de otro para mantener bajo el throttling de la API, y una llamada que falla con &lt;code&gt;AccessDenied&lt;/code&gt; se anota y se salta, sin detener nunca la ejecución. Los resultados se convierten en un grafo dirigido de &lt;code&gt;networkx&lt;/code&gt; envuelto en modelos de Pydantic, con aristas tipadas (&lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;routes_to&lt;/code&gt;, &lt;code&gt;attached_to&lt;/code&gt;, &lt;code&gt;peered_with&lt;/code&gt;, &lt;code&gt;shared_from&lt;/code&gt;) y las cuentas externas como nodos propios. Las posiciones se calculan una sola vez, en una cuadrícula determinista: nube, región, VPC, zona de disponibilidad, subred. Los dos exportadores de diagramas dibujan a partir de ese único layout, así que la misma entrada da el mismo dibujo, nada se mueve entre dos ejecuciones salvo que se haya movido la red, y una diferencia significa algo. Los exportadores escriben XML de mxGraph con las formas &lt;code&gt;mxgraph.aws4&lt;/code&gt; para draw.io, un paquete Lucid Standard Import para Lucid, y el inventario en JSON, Markdown y HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Las decisiones que llevaron tiempo
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Flujos numerados.&lt;/strong&gt; Todas las flechas son del mismo gris, y unos círculos numerados distinguen los tipos de tráfico. Cada tipo de tráfico conserva su número en todas las páginas: 1 entrada desde Internet, 2 salida por NAT, 3 attachment de Transit Gateway, 4 Direct Connect o VPN, 5 peering de VPC, 6 lista de prefijos compartida o RAM. Una leyenda enumera los que usa cada página. Los números salen del tipo de cada arista, no del tráfico observado, y la documentación lo dice así.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La página global.&lt;/strong&gt; Toda la red en una página: cuentas, el hub de Transit Gateway, peerings, gateways de Direct Connect. Lo que aparece citado pero no pertenece a una cuenta auditada va en un recuadro de línea discontinua, uno por cada cuenta externa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Huecos, no ceros.&lt;/strong&gt; Una llamada denegada se convierte en un hueco registrado. Donde iría un recuento, la página pone &lt;code&gt;not read&lt;/code&gt; (literal, en inglés). Nunca escribe &lt;code&gt;0&lt;/code&gt;, porque un cero parece una respuesta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Páginas abarrotadas.&lt;/strong&gt; El layout es donde más tiempo se me fue. Una página de región con 77 API Gateways tardaba 97 segundos en &lt;code&gt;--detail full&lt;/code&gt;; dibujar los abanicos de aristas como un bus, un tronco con una rama por icono, la dejó en menos de uno. &lt;code&gt;--detail simple&lt;/code&gt; pliega más de tres recursos de un mismo tipo en una sola tarjeta, como &lt;code&gt;API Gateway ×77&lt;/code&gt;, y en la misma página pasó de 37 segundos a menos de uno, y de 17.700 a 2.660 píxeles de ancho.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fvpc-prod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fvpc-prod.png" alt="El diagrama de la VPC prod en draw.io, con subredes, tablas de rutas y flujos numerados" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Una página de VPC: subredes, enrutamiento y flujos numerados.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pruébalo en una cuenta sandbox
&lt;/h2&gt;

&lt;p&gt;Necesitas Python 3.12 o superior y una cuenta de AWS que puedas leer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas
vpcatlas iam-policy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; auditor-policy.json   &lt;span class="c"&gt;# asóciala al rol o usuario que vayas a usar&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;--profile&lt;/span&gt; audit &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ./out
vpcatlas serve &lt;span class="nt"&gt;--cache&lt;/span&gt; ./out/vpcatlas-graph.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para una prueba rápida en un sandbox también sirve la política gestionada de AWS &lt;code&gt;ReadOnlyAccess&lt;/code&gt;, pero lee mucho más de lo que la herramienta necesita; no la dejes en un rol permanente. Las credenciales funcionan como las de la CLI de AWS: un perfil, variables de entorno, SSO o un rol asumido con external ID y MFA.&lt;/p&gt;

&lt;p&gt;Obtienes el grafo, el inventario y una carpeta &lt;code&gt;diagrams/&lt;/code&gt;. Abre cualquier fichero &lt;code&gt;.drawio&lt;/code&gt; en diagrams.net o importa un &lt;code&gt;.lucid&lt;/code&gt; con Lucid Standard Import. Empieza por &lt;code&gt;vpcatlas-global.drawio&lt;/code&gt;. El panel se abre en &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt; sobre el escaneo que acabas de hacer, sin llamar a AWS otra vez.&lt;/p&gt;

&lt;p&gt;Hay un instalador binario que no necesita Python (&lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt;, o una línea de PowerShell en Windows) y una imagen Docker, que sirve el panel por defecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:8080:8080 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.aws:/home/atlas/.aws:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  vpcatlas/vpcatlas serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--profile&lt;/span&gt; audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El panel muestra la topología de tu red, así que mantenlo en una red privada, o define &lt;code&gt;VPCATLAS_AUTH_PASSWORD&lt;/code&gt; y sírvelo por HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que aún no hace
&lt;/h2&gt;

&lt;p&gt;Es una alfa, y los límites están en la documentación.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Las cuentas se listan a mano.&lt;/strong&gt; Repite &lt;code&gt;--profile&lt;/code&gt; o &lt;code&gt;--role-arn&lt;/code&gt; para fusionar varias cuentas en un grafo. Todavía no descubre las cuentas de una AWS Organization: las listas tú.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una instantánea.&lt;/strong&gt; Los ficheros describen la red en el momento del escaneo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin análisis de tráfico.&lt;/strong&gt; Ni flow logs, ni análisis de alcanzabilidad, ni simulación de rutas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin contenidos.&lt;/strong&gt; Ni objetos de S3, ni secretos, ni eventos de log, ni el contenido de las reglas o políticas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Servicios sin colector.&lt;/strong&gt; Global Accelerator, VPC Lattice, IPAM, Network Manager y Cloud WAN, Client VPN y alguno más.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo AWS&lt;/strong&gt;, y un fichero regenerado sustituye tus ediciones, así que guarda las copias editadas con otro nombre.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Previsto para la v2: topologías de Network Manager y Cloud WAN, trazado de rutas de Direct Connect y VPN, deriva entre dos instantáneas, diferencias de diagramas en pull requests y ejecuciones multicuenta a partir de la lista de AWS Organizations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se construye y se publica
&lt;/h2&gt;

&lt;p&gt;Quería poder pedirle a esta herramienta lo que le pido a cualquier otra: ¿de dónde ha salido el binario?&lt;/p&gt;

&lt;p&gt;Cada cambio pasa por lint, comprobación de tipos, tests en Linux, macOS y Windows, las dos imágenes Docker con pruebas de humo y análisis, y los instaladores en tres sistemas. CodeQL, OpenSSF Scorecard y Dependabot corren cada semana; &lt;code&gt;pip-audit&lt;/code&gt;, cada noche. La lista completa, y por qué está cada comprobación, está en &lt;a href="https://vpcatlas.com/es/blog/que-hace-falta-para-fiarse-del-binario-de-una-cli-de-una-sola-persona/" rel="noopener noreferrer"&gt;el post del CI/CD&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Una release produce binarios para hasta cinco plataformas, &lt;code&gt;SHA256SUMS&lt;/code&gt;, un SBOM e imágenes firmadas y con atestación, y un último job instala la release publicada con los instaladores que sirve la web. El paquete está en PyPI y la imagen en Docker Hub, para linux/amd64 y linux/arm64. Para comprobar una descarga:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://vpcatlas.com/releases/vX.Y.Z/SHA256SUMS
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las firmas de imagen y los SBOM empiezan en la 0.4.1, y verificar la firma de una imagen necesita cosign 3 o superior.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Actualización, 23 de octubre de 2026:&lt;/em&gt; el repositorio del código es privado por ahora. Las releases se copian a &lt;code&gt;vpcatlas.com/releases&lt;/code&gt; y los instaladores descargan de ahí; una descarga se comprueba con &lt;code&gt;SHA256SUMS&lt;/code&gt; como arriba (las imágenes y el chart, con cosign), y los pasos que necesitan un repositorio público (atestaciones, CodeQL, el último job de instalación) están en pausa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde conseguirla
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instalación: &lt;a href="https://vpcatlas.com/docs/install/" rel="noopener noreferrer"&gt;https://vpcatlas.com/docs/install/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentación: &lt;a href="https://vpcatlas.com/docs/" rel="noopener noreferrer"&gt;https://vpcatlas.com/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Hub: &lt;a href="https://hub.docker.com/r/vpcatlas/vpcatlas" rel="noopener noreferrer"&gt;https://hub.docker.com/r/vpcatlas/vpcatlas&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tiene licencia MIT. Lo más útil que puedes hacer es ejecutarla en una cuenta sandbox y decirme dónde está mal el dibujo o dónde la lectura se queda corta.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>networking</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Nobody keeps the AWS network diagram current. I built a tool for it</title>
      <dc:creator>Karina Pérez</dc:creator>
      <pubDate>Sun, 18 Oct 2026 19:08:12 +0000</pubDate>
      <link>https://dev.to/karinapp/nobody-keeps-the-aws-network-diagram-current-i-built-a-tool-for-it-284d</link>
      <guid>https://dev.to/karinapp/nobody-keeps-the-aws-network-diagram-current-i-built-a-tool-for-it-284d</guid>
      <description>&lt;p&gt;VPC Atlas reads an AWS account with read-only calls and draws its network in draw.io and Lucidchart. Alpha, MIT licensed.&lt;/p&gt;

&lt;p&gt;Every team I joined asked for the same thing first: the network diagram. The one that shows every VPC, how the VPCs connect through Transit Gateways, peerings, VPNs and Direct Connect, and what sits at the edge in front of them. And every time, the diagram that existed was out of date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawn by hand, or rebuilt from the console
&lt;/h2&gt;

&lt;p&gt;A network diagram drawn by hand is wrong a week later. Someone adds an attachment, someone shares a Transit Gateway through AWS RAM, a peering with another account appears, and the picture stays as it was.&lt;/p&gt;

&lt;p&gt;The alternative is to rebuild it from the console: open each VPC, read the route tables, follow the attachments, check the endpoints, then draw it. On a real account that takes days. I wanted to point a tool at the account, get the diagram in minutes, and do it again next week to compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  What VPC Atlas does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;vpcatlas&lt;/code&gt; connects to an AWS account with auditor permissions and maps its network: VPCs, subnets, routing, gateways, endpoints, load balancers, firewalls, the DNS resolver and whatever is shared through RAM, plus what sits in front of them: Route 53, CloudFront, WAF, ACM and Shield. It also reads compute and data stores such as RDS, S3 and DynamoDB, so a subnet shows what runs in it, and account services such as CloudTrail, GuardDuty and KMS for the inventory.&lt;/p&gt;

&lt;p&gt;It is read-only by design. It calls &lt;code&gt;Describe*&lt;/code&gt;, &lt;code&gt;Get*&lt;/code&gt;, &lt;code&gt;List*&lt;/code&gt; and &lt;code&gt;Search*&lt;/code&gt; APIs and never creates, changes or deletes anything. It ships the minimal policy it needs, so you can read what it asks for before you grant it.&lt;/p&gt;

&lt;p&gt;One run gives you three things. The diagrams: a Global page, one page per Region and one page per VPC, as &lt;code&gt;.drawio&lt;/code&gt; and &lt;code&gt;.lucid&lt;/code&gt; files, drawn with the AWS Architecture Icons and laid out like the AWS Managed Services multi-account landing zone network reference. An inventory in JSON, Markdown and HTML, with every resource account by account and a section for every external account linked to yours. And a dashboard: &lt;code&gt;vpcatlas serve&lt;/code&gt; shows the same diagrams in the browser, keeps a history of scans and draws the difference between two of them, what was added in green and what changed in orange. The raw result is saved too, as &lt;code&gt;vpcatlas-graph.json&lt;/code&gt;, so you can redraw later without calling AWS again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fdemo.gif" alt="Recording of vpcatlas discover running against a simulated AWS account, then listing the generated diagram files" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recordings and diagrams here come from a simulated AWS account, so the ids are examples. This is the dashboard on that account:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphd6cwgyzijat2sa463q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fphd6cwgyzijat2sa463q.png" alt="The vpcatlas dashboard showing the live diagrams, run history and downloads" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And a Region page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8roh4u579a7of4jzspe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8roh4u579a7of4jzspe.png" alt="The eu-west-1 region diagram in draw.io: VPCs, Transit Gateway, endpoints and gateways" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;A read-only pipeline with four stages: collect, graph, layout, export. Each stage takes one input and gives one output, which is what makes it easy to test.&lt;/p&gt;

&lt;p&gt;Collectors read the account, one per service, all paginated. Regions run in parallel, but inside a Region the collectors run one after another to keep API throttling low, and a call that fails with &lt;code&gt;AccessDenied&lt;/code&gt; is logged and skipped, without ever stopping the run. The results become a &lt;code&gt;networkx&lt;/code&gt; directed graph wrapped by Pydantic models, with typed edges (&lt;code&gt;contains&lt;/code&gt;, &lt;code&gt;routes_to&lt;/code&gt;, &lt;code&gt;attached_to&lt;/code&gt;, &lt;code&gt;peered_with&lt;/code&gt;, &lt;code&gt;shared_from&lt;/code&gt;) and external accounts as nodes of their own. Positions are computed once, as a deterministic grid: cloud, Region, VPC, availability zone, subnet. Both diagram exporters draw from that one layout, so the same input gives the same drawing, nothing moves between two runs unless the network moved, and a diff means something. The exporters write mxGraph XML with the &lt;code&gt;mxgraph.aws4&lt;/code&gt; shapes for draw.io, a Lucid Standard Import package for Lucid, and the inventory in JSON, Markdown and HTML.&lt;/p&gt;

&lt;h3&gt;
  
  
  The decisions that took time
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Numbered flows.&lt;/strong&gt; Every arrow is the same grey, and numbered circles tell the kinds of traffic apart. A kind of traffic keeps its number on every page: 1 Internet ingress, 2 NAT egress, 3 Transit Gateway attachment, 4 Direct Connect or VPN, 5 VPC peering, 6 shared prefix list or RAM. A legend lists the ones a page uses. The numbers come from the kind of each edge, not from observed traffic, and the documentation says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Global page.&lt;/strong&gt; The whole network on one page: accounts, the Transit Gateway hub, peerings, Direct Connect gateways. Anything referenced but not owned by an audited account goes in a dashed box of its own, one per external account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gaps, not zeros.&lt;/strong&gt; A denied call becomes a recorded gap. Where a count would have been, the page says &lt;code&gt;not read&lt;/code&gt;. It never writes &lt;code&gt;0&lt;/code&gt;, because a zero looks like an answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crowded pages.&lt;/strong&gt; The layout is where most of my time went. A Region page with 77 API Gateways took 97 seconds in &lt;code&gt;--detail full&lt;/code&gt;; drawing the fans of edges as a bus, one trunk with a branch per icon, brought it to under a second. &lt;code&gt;--detail simple&lt;/code&gt; folds more than three resources of one kind into one card, such as &lt;code&gt;API Gateway ×77&lt;/code&gt;, and on the same page it went from 37 seconds to under a second, and from 17,700 to 2,660 pixels wide.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fvpc-prod.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkarina-pp%2Fvpcatlas%2Fmain%2Fdocs%2Fassets%2Fvpc-prod.png" alt="The diagram of the prod VPC in draw.io, with subnets, route tables and numbered flows" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One VPC page: subnets, routing and numbered flows.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in a sandbox account
&lt;/h2&gt;

&lt;p&gt;You need Python 3.12 or newer and an AWS account you may read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vpcatlas
vpcatlas iam-policy &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; auditor-policy.json   &lt;span class="c"&gt;# attach this to the role or user you will use&lt;/span&gt;
vpcatlas discover &lt;span class="nt"&gt;--profile&lt;/span&gt; audit &lt;span class="nt"&gt;--output-dir&lt;/span&gt; ./out
vpcatlas serve &lt;span class="nt"&gt;--cache&lt;/span&gt; ./out/vpcatlas-graph.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a quick run in a sandbox, the AWS managed &lt;code&gt;ReadOnlyAccess&lt;/code&gt; also works, but it reads far more than the tool needs; don't leave it on a role you keep. Credentials work the way the AWS CLI's do: a profile, environment variables, SSO, or an assumed role with an external ID and MFA.&lt;/p&gt;

&lt;p&gt;You get the graph, the inventory and a &lt;code&gt;diagrams/&lt;/code&gt; folder. Open any &lt;code&gt;.drawio&lt;/code&gt; file in diagrams.net, or import a &lt;code&gt;.lucid&lt;/code&gt; file with Lucid Standard Import. Start with &lt;code&gt;vpcatlas-global.drawio&lt;/code&gt;. The dashboard opens on &lt;a href="http://127.0.0.1:8080" rel="noopener noreferrer"&gt;http://127.0.0.1:8080&lt;/a&gt; over the scan you just made, without calling AWS again.&lt;/p&gt;

&lt;p&gt;There is a binary installer with no Python needed (&lt;code&gt;curl -fsSL https://vpcatlas.com/install.sh | sh&lt;/code&gt;, or a PowerShell one-liner on Windows) and a Docker image, which serves the dashboard by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:8080:8080 &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.aws:/home/atlas/.aws:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  vpcatlas/vpcatlas serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--profile&lt;/span&gt; audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard shows your network topology, so keep it on a private network, or set &lt;code&gt;VPCATLAS_AUTH_PASSWORD&lt;/code&gt; and serve it over HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do yet
&lt;/h2&gt;

&lt;p&gt;This is an alpha, and the limits are in the documentation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Accounts are listed by hand.&lt;/strong&gt; Repeat &lt;code&gt;--profile&lt;/code&gt; or &lt;code&gt;--role-arn&lt;/code&gt; to merge several accounts into one graph. There is no discovery of the accounts of an AWS Organization yet: you list them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A snapshot.&lt;/strong&gt; The files describe the network at the moment of the scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No traffic analysis.&lt;/strong&gt; No flow logs, reachability analysis or route simulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No contents.&lt;/strong&gt; No S3 objects, secrets or log events, and no rule or policy bodies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services without a collector.&lt;/strong&gt; Global Accelerator, VPC Lattice, IPAM, Network Manager and Cloud WAN, Client VPN and a few more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS only&lt;/strong&gt;, and a regenerated file replaces your edits, so keep edited copies under another name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Planned for v2: Network Manager and Cloud WAN topologies, Direct Connect and VPN path tracing, drift between two snapshots, diagram diffs in pull requests, and multi-account runs from an AWS Organizations list.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is built and released
&lt;/h2&gt;

&lt;p&gt;I wanted to be able to ask of this tool what I ask of any other: where did the binary come from?&lt;/p&gt;

&lt;p&gt;Every change goes through lint, type checks, tests on Linux, macOS and Windows, both Docker images with smoke tests and a scan, and the installers on three systems. CodeQL, OpenSSF Scorecard and Dependabot run weekly, &lt;code&gt;pip-audit&lt;/code&gt; nightly. The full list, and the reason for each check, is in &lt;a href="https://vpcatlas.com/blog/what-a-one-person-cli-needs-before-you-can-trust-its-binary/" rel="noopener noreferrer"&gt;the CI/CD post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A release produces binaries for up to five platforms, &lt;code&gt;SHA256SUMS&lt;/code&gt;, an SBOM, and signed and attested images, and a last job installs the published release with the live installers. The package is on PyPI and the image on Docker Hub, for linux/amd64 and linux/arm64. To check a download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://vpcatlas.com/releases/vX.Y.Z/SHA256SUMS
&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS &lt;span class="nt"&gt;--ignore-missing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Image signatures and SBOMs start with 0.4.1, and verifying an image signature needs cosign 3 or newer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Update, 23 October 2026:&lt;/em&gt; the source repository is private for now. Releases are mirrored to &lt;code&gt;vpcatlas.com/releases&lt;/code&gt; and the installers download from there; a download is checked against &lt;code&gt;SHA256SUMS&lt;/code&gt; as above (the images and the chart with cosign), and the steps that need a public repository (attestations, CodeQL, the last install job) are paused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install: &lt;a href="https://vpcatlas.com/docs/install/" rel="noopener noreferrer"&gt;https://vpcatlas.com/docs/install/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://vpcatlas.com/docs/" rel="noopener noreferrer"&gt;https://vpcatlas.com/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Hub: &lt;a href="https://hub.docker.com/r/vpcatlas/vpcatlas" rel="noopener noreferrer"&gt;https://hub.docker.com/r/vpcatlas/vpcatlas&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's MIT licensed. The most useful thing you can do is run it on a sandbox account and tell me where the drawing is wrong or the reading comes up short.&lt;/p&gt;

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