<?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>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;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;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;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;, 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; y el código en el &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;repositorio&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;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;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;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;, 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;, and the code in the &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;repository&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;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;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;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;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;h2&gt;
  
  
  Dónde mirar
&lt;/h2&gt;

&lt;p&gt;Los workflows están en el &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;repositorio&lt;/a&gt;, cada ejecución aparece en su página de Actions 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;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;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;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;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;h2&gt;
  
  
  Where to look
&lt;/h2&gt;

&lt;p&gt;The workflows are in the &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;repository&lt;/a&gt;, every run is on its Actions page, 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="799" height="420"&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="847"&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;gh attestation verify vpcatlas-X.Y.Z-linux-x86_64.tar.gz &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
&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;h2&gt;
  
  
  Hecha con IA, funciona sin ella
&lt;/h2&gt;

&lt;p&gt;La IA me ayudó a escribir esta herramienta, y prefiero decirlo a fingir lo contrario. La herramienta en sí no necesita nada de eso: ni modelo, ni clave de API, ni cuenta que abrir en ningún sitio, ni factura por token, y nada sale de tu red salvo las llamadas a la API de AWS que tú autorizaste. Hay equipos que no pueden usar herramientas de IA en el trabajo, o que no las van a pagar. Siguen necesitando el diagrama, así que la herramienta no depende de ninguna.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Código y releases: &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;https://github.com/karina-pp/vpcatlas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Web: &lt;a href="https://vpcatlas.com" rel="noopener noreferrer"&gt;https://vpcatlas.com&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="799" height="420"&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="847"&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;gh attestation verify vpcatlas-X.Y.Z-linux-x86_64.tar.gz &lt;span class="nt"&gt;--owner&lt;/span&gt; karina-pp
&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;h2&gt;
  
  
  Built with AI, runs without it
&lt;/h2&gt;

&lt;p&gt;AI helped me write this tool, and I'd rather say so than pretend otherwise. The tool itself needs none of it: no model, no API key, no account with anyone, no bill per token, and nothing leaves your network except the AWS API calls you authorised. Some teams can't use AI tools at work, or won't pay for them. They still need the diagram, so the tool doesn't depend on one.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Source and releases: &lt;a href="https://github.com/karina-pp/vpcatlas" rel="noopener noreferrer"&gt;https://github.com/karina-pp/vpcatlas&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://vpcatlas.com" rel="noopener noreferrer"&gt;https://vpcatlas.com&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>
