<?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: Alex </title>
    <description>The latest articles on DEV Community by Alex  (@upstood).</description>
    <link>https://dev.to/upstood</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%2F3867440%2F0f92e308-f422-407d-b558-73be7c120c4a.png</url>
      <title>DEV Community: Alex </title>
      <link>https://dev.to/upstood</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/upstood"/>
    <language>en</language>
    <item>
      <title>Terraform Modules vs CDK Constructs | What the Extra Layers Change</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Wed, 29 Jul 2026 17:14:48 +0000</pubDate>
      <link>https://dev.to/upstood/terraform-modules-vs-cdk-constructs-what-the-extra-layers-change-daa</link>
      <guid>https://dev.to/upstood/terraform-modules-vs-cdk-constructs-what-the-extra-layers-change-daa</guid>
      <description>&lt;p&gt;Terraform gives you two layers of abstraction. CDK gives you four. That difference decides how much your own module has to justify itself, and what a refactor costs you a year later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Teams moving between the two tools compare the syntax. HCL against TypeScript, &lt;code&gt;terraform plan&lt;/code&gt; against &lt;code&gt;cdk diff&lt;/code&gt;, state files against CloudFormation. The syntax is the smallest difference of the three.&lt;/p&gt;

&lt;p&gt;The larger one is how many layers sit between a line of your code and a resource in the account, and where that resource's identity comes from. In Terraform the identity is something you write. In CDK it is something the layer structure computes for you.&lt;/p&gt;

&lt;p&gt;That sounds academic until a pull request that changes no behaviour deletes a bucket.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform has two layers. CDK has four.
&lt;/h3&gt;

&lt;p&gt;In Terraform there is the provider resource, and there is the module you write around it. That is the entire ladder. When a guide says "write a module", it means: wrap raw resources, add defaults, validate inputs, expose outputs.&lt;/p&gt;

&lt;p&gt;CDK adds two rungs before you write anything.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;L1&lt;/strong&gt; (&lt;code&gt;CfnBucket&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Auto-generated from the CloudFormation resource spec. One construct, one resource, no defaults, raw property shapes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;L2&lt;/strong&gt; (&lt;code&gt;Bucket&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Hand-written by AWS. Opinionated defaults, typed enums, helper methods. Can emit several resources.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;L3&lt;/strong&gt; (patterns)&lt;/td&gt;
&lt;td&gt;Several L2s wired together for a use case.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your wrapper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Whatever your organisation adds on top.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The consequence is easy to miss: an L2 already is a module in the Terraform sense. Curated, opinionated, typed inputs, sensible defaults, maintained by someone else. When you write your own construct around &lt;code&gt;s3.Bucket&lt;/code&gt;, you are writing a module around a module.&lt;/p&gt;

&lt;p&gt;The layer count is not just conceptual, it shows up in the template. A plain L1 &lt;code&gt;CfnBucket&lt;/code&gt; synthesises to exactly one resource. The L2 &lt;code&gt;Bucket&lt;/code&gt; with &lt;code&gt;enforceSSL: true&lt;/code&gt; and &lt;code&gt;autoDeleteObjects: true&lt;/code&gt; synthesises to five: the bucket, a bucket policy, a custom resource, an IAM role and a Lambda function. One line of props, four extra resources, one of which executes code in the account.&lt;/p&gt;

&lt;p&gt;Neither number is wrong. They are different amounts of decision made on your behalf.&lt;/p&gt;

&lt;h3&gt;
  
  
  Identity is where the layers bite
&lt;/h3&gt;

&lt;p&gt;In Terraform, a resource's identity is the address you wrote: &lt;code&gt;aws_s3_bucket.bucket&lt;/code&gt;. It is in the file. You can grep for it.&lt;/p&gt;

&lt;p&gt;In CDK, identity is the CloudFormation logical ID, and CDK computes it by hashing the construct's path through those layers. You never write it. It does not appear anywhere in your source code.&lt;/p&gt;

&lt;p&gt;Here is the same bucket, &lt;code&gt;bucketName: "demo"&lt;/code&gt; throughout, synthesised on aws-cdk-lib 2.189.1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change to the code&lt;/th&gt;
&lt;th&gt;Logical ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;baseline: &lt;code&gt;new s3.Bucket(this, "Bucket", …)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bucket83908E77&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;added &lt;code&gt;versioned&lt;/code&gt;, a lifecycle rule, a new output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bucket83908E77&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;renamed the TypeScript class&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Bucket83908E77&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;renamed the construct id to &lt;code&gt;"Storage"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Storage07F31EBC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;extracted the bucket into a &lt;code&gt;SecureBucket&lt;/code&gt; construct&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BucketD7FEB781&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;added a grouping parent construct&lt;/td&gt;
&lt;td&gt;&lt;code&gt;StorageBucket5CB7C8EA&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Properties are free. Class names, file names and variable names are free. What is not free is the id strings on the path from the stack down to the resource, and how many levels sit between them.&lt;/p&gt;

&lt;p&gt;The middle three rows are the interesting ones. Adding versioning and a lifecycle rule changes real infrastructure behaviour and the identity holds. Extracting a construct changes no behaviour at all and the identity moves.&lt;/p&gt;

&lt;h3&gt;
  
  
  What CloudFormation does with that
&lt;/h3&gt;

&lt;p&gt;It matches resources on the logical ID alone. Not on the bucket name, not on the properties. So the extraction reads as one resource removed and a different one added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;[-] AWS::S3::Bucket Bucket        Bucket83908E77   destroy
[+] AWS::S3::Bucket Bucket/Bucket BucketD7FEB781
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cdk diff&lt;/code&gt; reports it accurately. The word &lt;code&gt;destroy&lt;/code&gt; is right there. The difficulty is upstream of the diff: the pull request that produced it contains no bucket name, no property change and no resource. It contains a class extraction and two changed lines, which is normally the safest kind of change a reviewer sees. And the two logical IDs both begin with &lt;code&gt;Bucket&lt;/code&gt;, differing only in eight hex characters.&lt;/p&gt;

&lt;p&gt;With the default removal policy the same diff reads &lt;code&gt;orphan&lt;/code&gt; instead of &lt;code&gt;destroy&lt;/code&gt;, which leaves the old bucket behind in the account, unmanaged and still billing. Which of the two you get depends on a removal policy set somewhere else in the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same trap has a name in Terraform
&lt;/h3&gt;

&lt;p&gt;Terraform has this problem too. Rename a resource inside a module and consumers get a destroy and create.&lt;/p&gt;

&lt;p&gt;The difference is that Terraform ships a repair tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;moved&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
  &lt;span class="nx"&gt;to&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That block lives inside the module. It travels with the version bump. A consumer who upgrades runs &lt;code&gt;plan&lt;/code&gt; and reads "has moved to", followed by no changes. Once every consumer is upgraded, the author deletes the block in a later major.&lt;/p&gt;

&lt;p&gt;CDK's equivalent arrived later and works differently. The &lt;code&gt;cdk refactor&lt;/code&gt; command, in preview and gated behind &lt;code&gt;--unstable=refactor&lt;/code&gt;, compares your code against the deployed state, detects constructs that have been renamed or moved, and uses CloudFormation's refactoring API to preserve the resources while their logical IDs change. AWS names this exact case in the command's documentation: "Reorganize your construct hierarchy (like grouping AWS resources under a new L3 construct) while preserving the underlying cloud resources."&lt;/p&gt;

&lt;p&gt;That closes the gap, but not in the same place &lt;code&gt;moved&lt;/code&gt; closes it. Terraform's block is written by the module author and travels inside the module, so a consumer repairs the break by upgrading and reading &lt;code&gt;plan&lt;/code&gt;. &lt;code&gt;cdk refactor&lt;/code&gt; is run by whoever owns the deployment, against deployed state, after the change has landed. For a construct published to other teams, the author can cause the break and cannot ship the fix.&lt;/p&gt;

&lt;p&gt;It also refuses to run on a mixed change. The command verifies that the application contains exactly the same set of resources as the deployed state, differing only in their location in the construct tree, and rejects the operation if it detects any resource additions, deletions or modifications. A pull request that extracts a construct and adjusts a property in the same commit is not refactorable by it.&lt;/p&gt;

&lt;p&gt;The older manual route is still there: reaching through the L2 to the L1 underneath and pinning the old value by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cfn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultChild&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CfnBucket&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;cfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overrideLogicalId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Bucket83908E77&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things separate that from &lt;code&gt;moved&lt;/code&gt;. You have to know the hash, which means synthesising the old version or reading the deployed template. It cannot be removed later, because removing it changes the identity again. And it documents nothing: the code says &lt;code&gt;SecureBucket/Bucket&lt;/code&gt; while claiming an identity from a shape that has not existed since the previous release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this compounds with the layer count
&lt;/h3&gt;

&lt;p&gt;The two findings are the same finding. Logical IDs are derived from the path through the layers, so every layer you add or remove is an identity change. Terraform's flatter ladder means fewer opportunities to move something by accident, and its repair is declarative, stable, and shipped by the author to the consumer. CDK has more rungs to move between, and its repair is a preview command run by the operator after the fact.&lt;/p&gt;

&lt;p&gt;CDK's extra rungs buy real things: the L2s carry AWS's own defaults, and the assertion tests that check them run in milliseconds with no cloud credentials, which is a tier most Terraform teams never reach. The rungs are also the mechanism by which a tidy-up deletes a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Keep the construct tree flat.&lt;/strong&gt; Resources sit directly in the stack. Fewer levels means fewer identity changes available. Composition happens by writing a new stack rather than restructuring an existing one. Cheap while nothing is deployed, and the decision is effectively frozen at the first deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pin logical IDs by hand.&lt;/strong&gt; &lt;code&gt;overrideLogicalId&lt;/code&gt; or &lt;code&gt;stack.renameLogicalId&lt;/code&gt;. Lets you restructure freely at the cost of a permanent hardcoded hash for every resource you move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diff the synthesised template in CI.&lt;/strong&gt; Synthesise the previous release and the current commit, extract the logical IDs from both, fail the build when an existing one disappears. Catches the class of change rather than repairing it, and it makes the invisible part visible in the pull request where the decision is actually made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retain on delete for stateful resources.&lt;/strong&gt; &lt;code&gt;RemovalPolicy.RETAIN&lt;/code&gt; turns a deletion into an orphan. The resource survives, unmanaged, and the deploy may still fail if the physical name is taken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cdk refactor&lt;/code&gt;.&lt;/strong&gt; Preview, behind &lt;code&gt;--unstable=refactor&lt;/code&gt;. Detects moved or renamed constructs and calls CloudFormation's refactoring API to keep the resources while their logical IDs change. &lt;code&gt;--dry-run&lt;/code&gt; prints the mapping without applying it, and an override file resolves cases where more than one mapping is valid. Costs you a preview dependency, and it rejects any change that is not purely a relocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one fits
&lt;/h2&gt;

&lt;p&gt;A flat tree fits work where nothing is deployed yet and the abstraction is not yet earned. The cost is that a shared construct extracted later is a migration, not a refactor. Before the first deploy that decision is free, and it stops being free permanently on the day something exists in the account.&lt;/p&gt;

&lt;p&gt;Hand-pinned logical IDs fit a small number of deliberate moves in an application stack you own end to end. They stop fitting in a shared construct library: the hashes accumulate across releases, and after the third one the tree's real identity lives in a pile of hex strings rather than in its shape.&lt;/p&gt;

&lt;p&gt;Template diffing in CI fits anyone publishing constructs that other teams consume by version. There, a refactor by the author is a destroy in someone else's account, and the author is the only person positioned to catch it. It fits less well on a single application stack with one reviewer who already reads every diff.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdk refactor&lt;/code&gt; fits a team that owns both the code and the deployment, can accept a preview command in the path to production, and is willing to split a restructuring commit from a behaviour commit so the command will accept it. It fits worst in the case that motivated this comparison: a construct library whose consumers are other teams. There the break travels with the version bump and the repair does not, so every consumer runs it separately in their own account, or does not run it at all.&lt;/p&gt;

&lt;p&gt;Retain on delete fits databases, state buckets and anything holding data, in every setup. What it does not do is prevent the identity change, so it pairs with one of the options above rather than replacing them.&lt;/p&gt;

&lt;p&gt;The layer question sits underneath all of this. If your organisation's wrapper adds real policy, naming guarantees and validated inputs that an L2 cannot express, the extra rung is doing work. If it forwards properties to &lt;code&gt;s3.Bucket&lt;/code&gt; with a longer name, it is a layer of identity risk that buys nothing, and Terraform's guidance applies unchanged: a module wrapping one resource with pass-through variables is not abstraction.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want us to look for issues like this in your account? We offer a free AWS audit: &lt;a href="https://www.upstood.com" rel="noopener noreferrer"&gt;upstood.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>terraform</category>
      <category>cdk</category>
      <category>aws</category>
    </item>
    <item>
      <title>NAT Gateway, VPC Endpoints, or an Egress Proxy: What Each Way Out of Your VPC Costs</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Tue, 21 Jul 2026 12:25:43 +0000</pubDate>
      <link>https://dev.to/upstood/nat-gateway-vpc-endpoints-or-an-egress-proxy-what-each-way-out-of-your-vpc-costs-nle</link>
      <guid>https://dev.to/upstood/nat-gateway-vpc-endpoints-or-an-egress-proxy-what-each-way-out-of-your-vpc-costs-nle</guid>
      <description>&lt;p&gt;Most private subnets reach the internet through a NAT gateway. It is metered per gigabyte, and it keeps no record of where those gigabytes went. Both of those facts carry a price, and there are two other ways to route the same traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Most private-subnet workloads reach the internet through a NAT gateway. It is one click in the console and it works, so it is rarely revisited after the day the VPC was built.&lt;/p&gt;

&lt;p&gt;A NAT gateway charges two ways. It costs $0.045 an hour just to exist, about $33 a month per gateway. It then costs another $0.045 for every gigabyte it processes (us-east-1, AWS VPC pricing, checked July 2026). The hourly charge is small. The data processing charge scales with traffic, and it applies to all outbound traffic, including traffic to AWS services that never needed to leave the AWS network at all.&lt;/p&gt;

&lt;p&gt;It also keeps no record of its own. A NAT gateway does not log which destinations your workloads reached, and it does not filter them. For a security or compliance review, the questions "what is this environment allowed to reach" and "what did it actually reach" have to be answered somewhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;Two separate costs come from the same property: everything leaves through one unfiltered, metered pipe.&lt;/p&gt;

&lt;p&gt;The first is that a large share of "internet" traffic is not internet traffic. Container image pulls, S3 reads, CloudWatch Logs, Secrets Manager calls: all of it is AWS traffic routed out through the NAT gateway and billed at $0.045 per gigabyte. Container platforms concentrate this. Every task launch and every scale-out pulls image layers, and on a busy cluster that is gigabytes an hour.&lt;/p&gt;

&lt;p&gt;Here is the math on a platform moving 2 TB of egress a month, split 1.4 TB of AWS-service traffic and 0.6 TB of genuine internet traffic. Through a single NAT gateway that is about $33 for the hourly charge, $90 for data processing on the full 2 TB, and $54 of data transfer out for the 0.6 TB that actually reaches the internet ($0.09 per gigabyte, first 10 TB, checked July 2026). Roughly $177 a month, of which $63 is processing charges on traffic that stayed inside AWS. At 20 TB of egress, the processing charge alone is $900 a month.&lt;/p&gt;

&lt;p&gt;The second cost is visibility, and it arrives as a separate bill. VPC Flow Logs capture IP addresses and byte counts. Route 53 Resolver query logs capture the domains that were looked up. Both are metered: log delivery is billed per gigabyte of logs, $0.50 to CloudWatch Logs or $0.25 to Amazon S3, plus storage (CloudWatch vended-logs pricing, checked July 2026). Adding GuardDuty to flag suspicious egress is another $1.00 per gigabyte analyzed for the first 500 GB a month (Amazon GuardDuty pricing, checked July 2026). What that buys is a record of IP addresses after the fact. Against destinations behind a CDN, where many services share rotating addresses, that record often does not identify who the workload talked to, and none of it blocks anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The options
&lt;/h2&gt;

&lt;p&gt;Three ways to route outbound traffic, and what each one costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The NAT gateway on its own
&lt;/h3&gt;

&lt;p&gt;Managed, highly available within an Availability Zone, and it handles any protocol. Nothing to patch, nothing to scale, no failure mode you own. It costs $0.045 an hour plus $0.045 per gigabyte processed, applied to every byte regardless of destination, and it provides no destination logging or filtering of its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC endpoints for AWS-service traffic
&lt;/h3&gt;

&lt;p&gt;A gateway endpoint for S3 costs nothing: no hourly charge, no data processing (AWS VPC pricing, checked July 2026). Because container image layers are stored in S3, it carries the heaviest AWS-bound flow most platforms have. Interface endpoints, including the two ECR endpoints (&lt;code&gt;ecr.api&lt;/code&gt; and &lt;code&gt;ecr.dkr&lt;/code&gt;) that complete a fully private image pull, cost $0.01 per hour per endpoint per Availability Zone plus $0.01 per gigabyte (AWS PrivateLink pricing, checked July 2026), under a quarter of the NAT processing rate (Amazon ECR docs, "Amazon ECR interface VPC endpoints").&lt;/p&gt;

&lt;p&gt;Endpoints only move AWS-service traffic. They do nothing for third-party APIs or package registries, and each interface endpoint adds a fixed hourly cost per Availability Zone whether or not it is busy. On the 2 TB example, moving AWS traffic onto endpoints takes the $90 processing charge down to a few dollars, against the endpoint hours added.&lt;/p&gt;

&lt;h3&gt;
  
  
  A self-managed egress proxy
&lt;/h3&gt;

&lt;p&gt;A forward proxy such as Squid on a small instance in a public subnet, with the private subnets given no default route to the internet at all. Applications reach outside only through the proxy via &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, which makes the proxy the single path out. A &lt;code&gt;t3.small&lt;/code&gt; runs about $15 a month.&lt;/p&gt;

&lt;p&gt;On cost, this takes the $0.045 per gigabyte NAT processing charge off internet-bound traffic. It does not touch data transfer out, which stays at $0.09 per gigabyte because those bytes genuinely leave AWS.&lt;/p&gt;

&lt;p&gt;On visibility, the proxy brokered the connection, so its log already carries the hostname and the allow or deny decision, and domains can be allow-listed. That is a different kind of record from flow logs, and it does not need a separate log-delivery and threat-detection pipeline to turn IP addresses back into names.&lt;/p&gt;

&lt;p&gt;What it costs elsewhere: it is infrastructure you own, keep available, patch and scale. It handles HTTP and HTTPS well and gets awkward with arbitrary TCP or UDP, which needs a transparent-proxy setup or a NAT gateway retained for those flows. It filters by domain on the TLS handshake and does not inspect inside encrypted connections without full interception, which carries its own risks and obligations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each one fits
&lt;/h2&gt;

&lt;p&gt;The NAT gateway's economics turn on volume. Below a few hundred gigabytes a month the processing charge is a rounding error and the hourly charge dominates, so the managed option is cheap in both money and attention. The crossover arrives when per-gigabyte processing outgrows the fixed cost of the alternatives: interface endpoint hours, or a proxy instance somebody maintains.&lt;/p&gt;

&lt;p&gt;The AWS-service share is the second variable. A workload that pulls container images constantly sends most of its "egress" to S3 and ECR, where a free gateway endpoint applies. A workload that mostly calls third-party APIs sees little from endpoints, and whatever it saves comes from the proxy path or from nowhere.&lt;/p&gt;

&lt;p&gt;Visibility moves independently of cost. If nothing needs to prove what the environment can reach, flow logs enabled on demand may be all the record required, and their per-gigabyte charge applies only while they run. If an auditor, a customer security review, or a data-residency obligation calls for a stated allow list and evidence of what actually left, that is not something a NAT gateway produces at any price, and the comparison stops being about dollars.&lt;/p&gt;

&lt;p&gt;Operational appetite is a real cost, not a soft one. A proxy is a component with an on-call story attached. A team without the capacity to own one is weighing an operational risk against a line on the bill, and which way that lands depends on the team, not on the architecture.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want us to look for issues like this in your account? We offer a free AWS audit: &lt;a href="https://www.upstood.com" rel="noopener noreferrer"&gt;upstood.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>networking</category>
    </item>
    <item>
      <title>Is Multi-AZ Enough for Disaster Recovery? What DORA Actually Asks of Your Application</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Wed, 15 Jul 2026 20:05:50 +0000</pubDate>
      <link>https://dev.to/upstood/is-multi-az-enough-for-disaster-recovery-what-dora-actually-asks-of-your-application-1fmi</link>
      <guid>https://dev.to/upstood/is-multi-az-enough-for-disaster-recovery-what-dora-actually-asks-of-your-application-1fmi</guid>
      <description>&lt;p&gt;Some companies run their disaster recovery across availability zones. Each AZ is a separate datacenter, so three AZs are three sites. Others maintain a second region, because to them a recovery site belongs somewhere else entirely.&lt;/p&gt;

&lt;p&gt;Both camps call it disaster recovery. Both have a point. And DORA, the regulation both camps worry about, does not pick a side.&lt;/p&gt;

&lt;p&gt;DORA never mentions availability zones or multi-region. It asks a different question: for each application, how long can you afford to be down, how much data can you afford to lose, and can you prove it? That has a different answer per application. This article shows what each setup actually protects against and what the regulation actually requires, so you can answer it for yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Ask two engineering teams about disaster recovery and you get two confident, opposite answers.&lt;/p&gt;

&lt;p&gt;The first team says: "we're multi-AZ". The reasoning is sound. AWS availability zones are physically separate datacenters, kilometers apart, with independent power, cooling, and physical security. Three AZs are three sites. Why would that not count as disaster recovery?&lt;/p&gt;

&lt;p&gt;The second team says: "we have a standby in a second region". Also sound. A disaster that takes out the whole region takes every AZ with it. A recovery site should not share fate with the primary.&lt;/p&gt;

&lt;p&gt;Then an auditor, a compliance framework, or a customer due-diligence questionnaire enters the room, usually citing DORA. It asks: what are your recovery objectives? Where is your secondary site? What happens if your provider's region fails? Both teams go looking for the sentence in the regulation that blesses their architecture. Neither finds it. It does not exist.&lt;/p&gt;

&lt;p&gt;So we went to the primary sources: the regulation text, AWS's own documentation, and the post-mortem of the largest cloud outage in recent memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. What DORA actually asks: objectives, not architecture
&lt;/h3&gt;

&lt;p&gt;DORA, the EU's Digital Operational Resilience Act for financial entities, is the strictest widely-applicable regime here. That makes it the useful worst case. We searched the full text of the regulation and its ICT risk management RTS. The words "availability zone" and "multi-region" never appear. "Region" appears exactly once, in a clause about disclosing where a vendor processes data.&lt;/p&gt;

&lt;p&gt;What DORA requires of a financial entity (Art. 12) is technology-neutral. Maintain "redundant ICT capacities equipped with resources, capabilities and functions that are adequate to ensure business needs". Restore backups on systems "physically and logically segregated from the source ICT system". Set recovery time and recovery point objectives per function, yourself, based on how critical the function is.&lt;/p&gt;

&lt;p&gt;DORA does contain a "secondary processing site" requirement, in Article 12(5). It applies to central securities depositories, and the accompanying technical standards extend it to central counterparties. Those are the market infrastructures the rest of the financial system runs on. The requirement does not bind a typical bank, and it does not bind a SaaS vendor.&lt;/p&gt;

&lt;p&gt;Even there, the language is about risk, not technology. The secondary site must sit "at a geographical distance from the primary processing site to ensure that it bears a distinct risk profile and to prevent it from being affected by the event which has affected the primary site". During the public consultation on the technical standards, respondents asked the three European Supervisory Authorities (EBA, ESMA and EIOPA, who drafted them) whether "secondary processing site" refers to secondary data centres. The authorities kept the risk-based wording and declined to name any technology (&lt;a href="https://www.esma.europa.eu/sites/default/files/2024-01/JC_2023_86_-_Final_report_on_draft_RTS_on_ICT_Risk_Management_Framework_and_on_simplified_ICT_Risk_Management_Framework.pdf" rel="noopener noreferrer"&gt;Final Report JC 2023 86&lt;/a&gt;, p. 173).&lt;/p&gt;

&lt;p&gt;If you are a SaaS company, you are most likely not in DORA's direct scope at all. You meet it indirectly, through contract clauses and due-diligence questionnaires from financial customers. So the question you have to answer is the regulator's question: does your recovery setup bear a distinct risk profile from your primary, per application, and can you prove your recovery objectives? Neither "we're multi-AZ" nor "we have a second region" answers that by itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. What the multi-AZ camp actually has
&lt;/h3&gt;

&lt;p&gt;The multi-AZ camp is right about the physical world, and AWS backs them.&lt;/p&gt;

&lt;p&gt;AZs are physically separate facilities, many kilometers apart, with independent power, cooling, and physical security. AWS's own disaster recovery whitepaper, &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-workloads-on-aws.html" rel="noopener noreferrer"&gt;Disaster Recovery of Workloads on AWS: Recovery in the Cloud&lt;/a&gt;, says plainly: "a multi-AZ architecture within an AWS Region may already meet much of your risk mitigation needs". A fire or a flood that takes out one datacenter is a disaster your multi-AZ architecture recovers from by design. For that risk profile, three AZs really are three sites.&lt;/p&gt;

&lt;p&gt;But the same whitepaper has a section titled, in full, &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/high-availability-is-not-disaster-recovery.html" rel="noopener noreferrer"&gt;"High availability is not disaster recovery"&lt;/a&gt;. The distinction: "Availability focuses on components of the workload, whereas disaster recovery focuses on discrete copies of the entire workload." And its &lt;a href="https://docs.aws.amazon.com/whitepapers/latest/disaster-recovery-workloads-on-aws/disaster-recovery-options-in-the-cloud.html" rel="noopener noreferrer"&gt;"Disaster recovery options in the cloud"&lt;/a&gt; section draws the boundary precisely: "If your definition of a disaster goes beyond the disruption or loss of a physical data center to that of a Region or if you are subject to regulatory requirements that require it, then you should consider Pilot Light, Warm Standby, or Multi-Site Active/Active."&lt;/p&gt;

&lt;p&gt;So AWS's position, in our words: multi-AZ is disaster recovery for datacenter-scale disasters. It is not disaster recovery for region-scale disasters. Which leaves one question: how often are disasters region-scale?&lt;/p&gt;

&lt;h3&gt;
  
  
  3. What the second-region camp actually has, and the outage that tested both
&lt;/h3&gt;

&lt;p&gt;On October 19-20, 2025, us-east-1 suffered roughly 14 hours of cascading impact. The root cause, from &lt;a href="https://aws.amazon.com/message/101925/" rel="noopener noreferrer"&gt;AWS's own post-event summary&lt;/a&gt;: "a latent race condition in the DynamoDB DNS management system that resulted in an incorrect empty DNS record for the service's regional endpoint". One automation bug deleted the DNS records for DynamoDB's regional endpoint. The failure cascaded into EC2 launches, network load balancers, Lambda, and STS. Downdetector logged over 6.5 million reports across more than 1,000 services.&lt;/p&gt;

&lt;p&gt;Here is what it means for the multi-AZ camp: every availability zone in the region was affected equally. The event was not a flood that your second AZ sits outside of. It was a software defect in a regional control plane, and all your AZs share that control plane. Against the failure mode that actually happened, multi-AZ provided zero isolation. And this keeps happening: December 2021, network congestion in us-east-1 disrupted sign-in globally. November 2020, a Kinesis failure took down Cognito authentication. The big cloud outages are software, not floods.&lt;/p&gt;

&lt;p&gt;But the same outage carries a warning for the second-region camp. Some global AWS services are homed in us-east-1: the IAM control plane, console sign-in and identity federation, parts of Route 53. During the outage, customers in other regions could not sign in with federated identities and could not modify IAM roles or policies. Redshift customers "in all AWS Regions were unable to use IAM user credentials for executing queries". AWS's own Support Center "successfully failed over to another region as designed", and then broke anyway, because a subsystem it depended on had not. A second region is not automatically a distinct risk profile either. It only is if the failover path has no hidden dependency on the failed region.&lt;/p&gt;

&lt;p&gt;So the honest scorecard. An availability zone gives you a distinct physical risk profile and an identical software risk profile. A second region gives you a distinct software risk profile only if you engineered the dependencies out. And the regulator's phrase "the event which has affected the primary site" covers both kinds of event.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Stop asking which camp is right. Decide per application, from recovery objectives. The technology follows.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Set RTO and RPO per application, as a business decision.&lt;/strong&gt; Recovery Time Objective: the maximum acceptable delay between interruption and restoration. Recovery Point Objective: the maximum acceptable data loss, measured in time. This is exactly what DORA asks the entity to define per function. Most companies have never written them down. That absence is the real audit finding, not the architecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Map each application to one of the four AWS strategies.&lt;/strong&gt; AWS's Well-Architected guidance (&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/rel_planning_for_recovery_disaster_recovery.html" rel="noopener noreferrer"&gt;Reliability Pillar, REL13-BP02&lt;/a&gt;) gives the honest numbers. Backup and restore: RPO in hours, RTO in 24 hours or less. Pilot light: RPO in minutes, RTO in tens of minutes. Warm standby: RPO in seconds, RTO in minutes. Multi-site active-active: RPO near zero, RTO potentially zero. Cost and operational complexity climb steeply along that ladder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The multi-AZ camp, done right: add cross-region backups.&lt;/strong&gt; Run highly available in one region. Continuously back up to a second region: segregated copies, restorable on segregated systems, which is the exact backup-segregation language regulators use. Accept an honest RTO measured in hours for the rare region-scale event. For every application whose true business tolerance is a day, this is a legitimate, defensible DR strategy, and dramatically cheaper than a standby region.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The second-region camp, done right: engineer out the hidden dependencies.&lt;/strong&gt; For the few applications that genuinely cannot be down for hours, build the standby region so it actually bears a distinct risk profile. Fail over using data plane operations only: control planes have lower availability design goals, and a control plane is exactly what broke in October. Use regional STS and sign-in endpoints, not the global ones homed in us-east-1. Pre-provision quotas and capacity. Make the failover decision independent of the failing region.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test it and write it down.&lt;/strong&gt; A recovery objective you have never tested is a guess. This is also the letter of the regulation: DORA Art. 11(6) requires testing continuity and recovery plans at least yearly, with scenarios that include "switchovers between the primary ICT infrastructure and the redundant capacity, backups and redundant facilities". The artifact that satisfies an auditor, and a prospect's due-diligence questionnaire, is documented objectives per application plus evidence of a tested recovery. Not an architecture diagram with two of everything.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;p&gt;Which camp is right? Both, for different applications.&lt;/p&gt;

&lt;p&gt;Multi-AZ plus segregated cross-region backups is a compliant, defensible answer for every application that can tolerate hours of recovery, and that is most of them. A properly engineered second region is the answer for the few that cannot. What DORA asks for is neither architecture. It asks for written recovery objectives per application, segregated restorable backups, and proof.&lt;/p&gt;

&lt;p&gt;Teams that adopt this framing stop overbuilding and underproving. DR spend becomes proportional to each application's actual tolerance, instead of one anxious architecture decision applied to everything. And the due-diligence answer changes. "We're multi-AZ" and "we have a second region" both invite follow-up questions. "Here are our recovery objectives per application and the test evidence" ends them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want us to look for issues like this in your account? We offer a free AWS audit: &lt;a href="https://www.upstood.com" rel="noopener noreferrer"&gt;upstood.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>infrastructure</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Session Manager removes your public IPs. Is it enough for an audit?</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Sun, 12 Jul 2026 13:49:54 +0000</pubDate>
      <link>https://dev.to/upstood/session-manager-removes-your-public-ips-is-it-enough-for-an-audit-bji</link>
      <guid>https://dev.to/upstood/session-manager-removes-your-public-ips-is-it-enough-for-an-audit-bji</guid>
      <description>&lt;p&gt;Every public IP on an EC2 instance exists for one of two reasons: the instance serves the internet, or somebody needs to SSH into it. The second reason stopped being valid years ago, and it is still the most common one we see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Instances get a public IP so an engineer can reach them over SSH. That single decision drags a chain of exposure behind it: a security group with port 22 open (often to 0.0.0.0/0 "temporarily"), key pairs shared over Slack and never rotated, an internet gateway route on subnets that never needed one, and a bastion host that is itself another public instance with another key pair.&lt;/p&gt;

&lt;p&gt;From an audit standpoint, every one of these is a finding. From an attacker's standpoint, every public IP is scanned within minutes of being assigned. And from an operations standpoint, none of it is necessary: AWS Systems Manager Session Manager gives you a shell on any instance with zero inbound ports, zero public IPs, and zero SSH keys.&lt;/p&gt;

&lt;p&gt;The biggest advantage deserves to be stated plainly: port 22 is not restricted, it is closed. Not narrowed to an office IP, not hidden behind a bastion: there is no listening port to protect at all, because access happens over the web. The shell opens in a browser tab from the AWS console (or from the CLI), authenticated by the IAM login the engineer already has.&lt;/p&gt;

&lt;p&gt;We have applied this in two very different settings, and the experience splits cleanly. In startups and small companies, Session Manager is a pure win: public IPs disappear, key management disappears, and engineers open a shell on any instance on the fly from the console or CLI. In enterprises, the network part is just as easy, but a different question stops the rollout: once everyone enters the VM through the same door, how do you prove who did what inside it? That question, and what to ship to the SIEM, is where most of this article lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;Session Manager inverts the connection model. The SSM Agent on the instance opens an outbound HTTPS connection to the Systems Manager service and waits. When you start a session, the service brokers it over that existing outbound channel. Nothing ever connects inbound to the instance, so there is nothing to expose. That inversion is what makes the browser shell possible: the operator's session rides the agent's outbound channel, which is why port 22 can stay closed on every instance in the fleet.&lt;/p&gt;

&lt;p&gt;Three things have to be true for this to work, and they are smaller than most teams expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The minimum instance role
&lt;/h3&gt;

&lt;p&gt;The instance needs an instance profile whose role allows the SSM Agent to talk to the service. The managed policy &lt;code&gt;AmazonSSMManagedInstanceCore&lt;/code&gt; is the supported baseline. If you want the actual minimum instead of the managed policy, this is what the agent needs for sessions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:UpdateInstanceInformation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:CreateDataChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenControlChannel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssmmessages:OpenDataChannel"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;s3:GetEncryptionConfiguration&lt;/code&gt; and &lt;code&gt;s3:PutObject&lt;/code&gt; on your logging bucket, or &lt;code&gt;logs:CreateLogStream&lt;/code&gt; and &lt;code&gt;logs:PutLogEvents&lt;/code&gt; on your logging group, if you enable session logging (you should, see below). Add &lt;code&gt;kms:GenerateDataKey&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; on the session KMS key if you encrypt sessions (you should as well).&lt;/p&gt;

&lt;p&gt;Note what is not in this policy: nothing. No inbound rule, no key material, no &lt;code&gt;ec2:*&lt;/code&gt;. The instance role is the entire server-side setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The minimum user policy
&lt;/h3&gt;

&lt;p&gt;On the operator side, scope &lt;code&gt;ssm:StartSession&lt;/code&gt; to the instances the person is allowed to reach. Tag-based scoping keeps this manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:StartSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ec2:*:*:instance/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"ssm:resourceTag/Environment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:StartSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:document/SSM-SessionManagerRunShell"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:TerminateSession"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:ResumeSession"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:session/${aws:username}-*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session-document statement matters: without pinning the document, a user who can start any session can bypass your logging preferences by choosing a different session document.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The network path (the "firewall")
&lt;/h3&gt;

&lt;p&gt;The agent needs outbound HTTPS to three service endpoints: &lt;code&gt;ssm&lt;/code&gt;, &lt;code&gt;ssmmessages&lt;/code&gt;, and &lt;code&gt;ec2messages&lt;/code&gt;. In a private subnet with no NAT, that means three interface VPC endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com.amazonaws.&amp;lt;region&amp;gt;.ssm
com.amazonaws.&amp;lt;region&amp;gt;.ssmmessages
com.amazonaws.&amp;lt;region&amp;gt;.ec2messages
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The security group on the endpoints allows inbound 443 from the instance security groups (or the VPC CIDR). The instance security group needs outbound 443 to the endpoint security group and nothing else. No inbound rules at all. If session logs go to S3 or CloudWatch, add the &lt;code&gt;s3&lt;/code&gt; gateway endpoint and the &lt;code&gt;logs&lt;/code&gt; interface endpoint; add &lt;code&gt;kms&lt;/code&gt; if sessions are encrypted.&lt;/p&gt;

&lt;p&gt;If the subnets already have a NAT gateway, the endpoints are optional (the agent will happily use the regional public endpoints over NAT), but the interface endpoints keep session traffic off the NAT data-processing meter and inside the VPC.&lt;/p&gt;

&lt;p&gt;With those three pieces in place, the public IPs, the bastion, port 22, and the key pairs are all deletable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it, and when an auditor pushes back
&lt;/h2&gt;

&lt;p&gt;Session Manager is the right default for interactive administrative access to EC2. It is not automatically audit-clean. If we were reviewing your account, this is what we would check.&lt;/p&gt;

&lt;p&gt;Use it when access is interactive, occasional, and administrative: debugging, incident response, one-off inspection. Every session start is a &lt;code&gt;StartSession&lt;/code&gt; event in CloudTrail tied to an IAM principal, which is already better evidence than an SSH login against a shared key.&lt;/p&gt;

&lt;p&gt;Turn on session logging before you call it a control. Out of the box, Session Manager records that a session happened, not what happened inside it. Session activity logging to S3 or CloudWatch Logs (with KMS encryption on both the session and the destination) captures the full terminal transcript. Without it, an auditor will correctly note that your privileged access has no content trail.&lt;/p&gt;

&lt;p&gt;The identity gap: everyone is &lt;code&gt;ssm-user&lt;/code&gt;. This is the finding most teams miss, and in our experience it is the single point where enterprise rollouts stall. Session Manager authenticates the AWS principal, but on the operating system every session lands as the same local account, &lt;code&gt;ssm-user&lt;/code&gt;, with sudo rights by default. Your OS logs, auditd trail, and file ownership all say &lt;code&gt;ssm-user&lt;/code&gt; did it. If your access policy or your regulator expects OS-level actions to be attributable to a named individual (and most do), the default setup fails that requirement even though AWS knows exactly who started the session.&lt;/p&gt;

&lt;p&gt;What the console identity already gives you. Before reaching for workarounds, know what is attributable out of the box. When a user starts a session from the console or CLI, CloudTrail records the &lt;code&gt;StartSession&lt;/code&gt; event with their full principal ARN, and the session ID itself is generated with the IAM user name as its prefix. This is why AWS's own example policies can scope &lt;code&gt;ssm:TerminateSession&lt;/code&gt; to &lt;code&gt;arn:aws:ssm:*:*:session/${aws:username}-*&lt;/code&gt;. Since the CloudWatch log stream for the session transcript is named after the session ID, the transcript of everything typed and returned in the session is already tied to the console identity by name. What AWS does not do is carry that identity into the operating system: there is no mechanism that logs the console IAM user into the box as themselves, and no automatic provisioning of OS accounts from IAM identities. Inside the VM, attribution stops at &lt;code&gt;ssm-user&lt;/code&gt; unless you configure Run As.&lt;/p&gt;

&lt;p&gt;Workarounds for the OS-level gap:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run As. Session Manager preferences can run sessions as a named OS user instead of &lt;code&gt;ssm-user&lt;/code&gt;, taken from the &lt;code&gt;SSMSessionRunAs&lt;/code&gt; tag on the IAM principal. Three constraints to plan around: it is Linux and macOS only, the &lt;code&gt;root&lt;/code&gt; account is not supported, and once Run As is on there is no fallback to &lt;code&gt;ssm-user&lt;/code&gt;. A missing tag or missing OS account means no session. From an audit perspective the no-fallback behavior is a feature: nobody can silently drop back to the shared identity.&lt;/li&gt;
&lt;li&gt;Run As works per person even under SSO. The obvious objection in a federated setup (Entra ID or any SAML IdP in front of AWS) is that everyone shares the same role, so a tag on the role would map everyone to the same OS user. The answer is session tags: the IdP passes &lt;code&gt;SSMSessionRunAs&lt;/code&gt; as a SAML attribute (&lt;code&gt;https://aws.amazon.com/SAML/Attributes/AccessControl:SSMSessionRunAs&lt;/code&gt;) with a per-user value, and each person's role session carries their own OS user name. With IAM Identity Center, enable Attributes for Access Control and map the attribute to &lt;code&gt;${path:userName}&lt;/code&gt; so it flows from the directory automatically. Same permission set, individual OS identity per human.&lt;/li&gt;
&lt;li&gt;The OS account still has to exist first. Session Manager verifies the target account before starting the session and refuses the connection if it is missing. It never creates users. That leaves an enterprise with exactly two options: provision matching Linux accounts on every node ahead of time (user data, Ansible, or a State Manager association syncing the user list), or domain-join the instances so the Run As target is a domain user that exists everywhere by definition. The domain-join path is the cleanest where AD is already present, and OS-level attribution then matches the corporate identity, but be precise about what it is: Session Manager checks that the domain account exists and runs the session as it. The mapping from AWS principal to domain user is tag-based trust, not a Kerberos logon, and an auditor should understand it as such. Note for Entra ID shops: Entra ID alone is not a joinable domain in the classic sense; this path needs real Active Directory in the picture (AWS Managed Microsoft AD, or hybrid AD synced through Entra Connect).&lt;/li&gt;
&lt;li&gt;Accept correlation as the control for small teams: session transcripts carry the IAM identity in their name (see above), CloudTrail carries the principal, and OS logs carry &lt;code&gt;ssm-user&lt;/code&gt; plus timestamps. Documenting that correlation procedure is defensible for low-change environments and startups. It is weak for regulated ones, because it degrades exactly when you need it most: concurrent sessions on the same instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When OS logs need to reach the SIEM
&lt;/h2&gt;

&lt;p&gt;Session transcripts in S3 are evidence, but they are not detection. Ship OS-level logs to the SIEM when any of these hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regulated workloads. If the instance is in scope for PCI DSS, ISO 27001 A.8.15/A.8.16, or similar, privileged session activity must be centrally collected and reviewed, not just stored. A transcript bucket nobody reads does not satisfy "monitored".&lt;/li&gt;
&lt;li&gt;Run As is your attribution control. Then &lt;code&gt;/var/log/secure&lt;/code&gt; and the auditd trail are where the named OS user actually appears, and they need to be tamper-evident and off-box. CloudWatch Agent to CloudWatch Logs, then a subscription filter to Kinesis Data Firehose into the SIEM, is the standard path and keeps the instance role additions minimal.&lt;/li&gt;
&lt;li&gt;You need alerting on what happens inside sessions, not just that they occurred: sudo to root, package installs, outbound connections initiated from an interactive shell. That is auditd or Sysmon territory, and it only has value if a detection pipeline consumes it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If none of those apply (dev accounts, no compliance scope, low blast radius), CloudTrail plus encrypted session transcripts is a proportionate control, and the SIEM integration can wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;p&gt;For a startup, the impact is immediate and unambiguous: public IPs deleted, port 22 closed everywhere, key pairs revoked, bastion terminated, and engineers reach any instance on the fly with nothing but their existing IAM login. The whole migration is an instance profile, three VPC endpoints, and one afternoon.&lt;/p&gt;

&lt;p&gt;For an enterprise, the network win is identical, but the honest impact statement includes the second phase: Run As backed by provisioned OS or domain accounts, session transcripts encrypted and centralized, and OS logs flowing to the SIEM where compliance scope demands it. That phase is real work. It is still less work than operating a bastion fleet with rotating SSH keys, and unlike the bastion, it ends with every privileged session attributable to a named person at both the AWS layer and the OS layer.&lt;/p&gt;

&lt;p&gt;The end state in both cases is the same: zero inbound ports for administrative access, and the answer to "why does this instance have a public IP" is only ever "it serves the internet".&lt;/p&gt;




&lt;p&gt;Want us to look for issues like this in your account? We offer a free AWS audit: &lt;a href="https://www.upstood.com" rel="noopener noreferrer"&gt;upstood.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>sessionmanager</category>
      <category>ssh</category>
    </item>
    <item>
      <title>Give Your Auditor AWS Access — Without Sharing a Single Credential</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:20:42 +0000</pubDate>
      <link>https://dev.to/upstood/give-your-auditor-aws-access-without-sharing-a-single-credential-140k</link>
      <guid>https://dev.to/upstood/give-your-auditor-aws-access-without-sharing-a-single-credential-140k</guid>
      <description>&lt;p&gt;If your auditor asks for that, that should be the first finding. 🚩&lt;/p&gt;

&lt;p&gt;An external auditor needs access to your AWS accounts, and the first instinct in most companies is one of two things: create an IAM user and share the access keys, or provision the auditor a user in your identity provider so they can log into the console.&lt;/p&gt;

&lt;p&gt;Neither is necessary. An auditor doesn’t need a login in your organization at all — API access is enough. And AWS has had a mechanism for exactly this situation for over a decade. It’s the same one AWS partners and every third-party monitoring tool use:&lt;/p&gt;

&lt;p&gt;A cross-account IAM role with a trust relationship. No credentials ever change hands.&lt;/p&gt;

&lt;p&gt;In this article I’ll walk through how the pattern works and share two CloudFormation templates I built — one for a single AWS account, one that covers an entire AWS Organization. You can deploy either in about five minutes. Both templates are also on GitHub: &lt;a href="https://github.com/up-cdk/aws-audit-access" rel="noopener noreferrer"&gt;https://github.com/up-cdk/aws-audit-access&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just create a user?
&lt;/h2&gt;

&lt;p&gt;Because every credential you create is a liability you now own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Access keys are long-lived. They work until someone remembers to delete them. Every audit I’ve read about that went wrong involved keys that outlived the engagement.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Passwords get shared insecurely. Email, Slack, a text message — the moment credentials travel, you’ve lost control of where they end up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;IdP users pollute your directory. An external party in your identity provider inherits whatever group memberships and app assignments someone clicks together, and deprovisioning is a manual step someone has to remember.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern: cross-account role + External ID
&lt;/h2&gt;

&lt;p&gt;The idea is simple. Instead of giving the auditor your credentials, you create a role in your account that the auditor’s own AWS account is allowed to assume.&lt;/p&gt;

&lt;p&gt;Four pieces make it safe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The trust policy names exactly one AWS account — the auditor’s. Not a user, not a password: an account-to-account trust. The auditor authenticates in their account with their credentials, then calls STS &lt;code&gt;AssumeRole&lt;/code&gt; to get temporary credentials for the role in yours.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An External ID guards the trust. This is a shared secret agreed between you and the auditor, set as a condition on the trust policy. Without presenting it, the role cannot be assumed — even by the trusted account. This protects against the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;confused deputy problem&lt;/a&gt;: if the auditor works with many clients, a malicious client can’t trick the auditor’s tooling into accessing your account, because they don’t know your External ID.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;All access is temporary. STS sessions expire (1 hour by default in these templates, configurable). There is no long-lived credential anywhere in the setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Everything is logged. Every &lt;code&gt;AssumeRole&lt;/code&gt; call and every API call the auditor makes lands in your CloudTrail, attributable to the audit role’s session. You can see exactly what was looked at, and when.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the permissions? Read-only, and scoped for the job: AWS’s managed &lt;code&gt;ReadOnlyAccess&lt;/code&gt; and &lt;code&gt;SecurityAudit&lt;/code&gt; policies, optionally &lt;code&gt;AWSBillingReadOnlyAccess&lt;/code&gt; for cost review, plus a small supplementary policy for audit-relevant services the managed policies miss (GuardDuty findings, Security Hub, Cost Explorer, Trusted Advisor, and a few others).&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: single account
&lt;/h2&gt;

&lt;p&gt;Deploy this template in the account being audited. Two parameters matter: the auditor’s 12-digit AWS account ID, and the External ID they give you.&lt;/p&gt;

&lt;p&gt;Template: aws-audit-readonly-role.yaml in the GitHub repo — &lt;a href="https://github.com/up-cdk/aws-audit-access" rel="noopener noreferrer"&gt;https://github.com/up-cdk/aws-audit-access&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deploy it from the console (CloudFormation → Create stack → Upload template), or from the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="nt"&gt;--template-file&lt;/span&gt; aws-audit-readonly-role.yaml &lt;span class="nt"&gt;--stack-name&lt;/span&gt; external-audit-access &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_NAMED_IAM  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="nv"&gt;AuditorAWSAccountId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;111122223333 &lt;span class="nv"&gt;ExternalId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;the-external-id-from-your-auditor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then send the auditor the role ARN from the stack outputs. That’s it — that’s the entire exchange. An ARN is not a secret; combined with the External ID (which the auditor already has, since they generated it) and the trust policy, it’s everything they need and nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: the entire AWS Organization
&lt;/h2&gt;

&lt;p&gt;Auditing one account is rare — most audits cover an Organization with anywhere from a handful to hundreds of member accounts. Creating the role by hand in each one doesn’t scale.&lt;/p&gt;

&lt;p&gt;The second template solves this with a service-managed CloudFormation StackSet. You deploy one stack in the management account, and it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;creates the audit role in the management account itself,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;creates a StackSet that rolls the same role out to every member account under the OUs you target (pass your root ID, e.g. &lt;code&gt;r-abcd&lt;/code&gt;, to cover the whole organization),&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;optionally auto-deploys to any new account added to those OUs later — no gaps if accounts are created mid-audit,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;and tears everything down centrally when you delete the stack.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One prerequisite: service-managed StackSets require trusted access between AWS Organizations and CloudFormation to be activated (CloudFormation console → StackSets → “Activate trusted access”, a one-time click in the management account).&lt;/p&gt;

&lt;p&gt;Template: aws-audit-organization-stackset.yaml in the same repo. The deploy command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws cloudformation deploy  --template-file aws-audit-organization-stackset.yaml --stack-name external-audit-access-org --capabilities CAPABILITY_NAMED_IAM --parameter-overrides AuditorAWSAccountId=111122223333 ExternalId=the-external-id-from-your-auditor TargetOrganizationalUnitIds=r-abcd DeploymentRegions=us-east-1&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;IAM is a global service, so one deployment region is enough for the stack instances.&lt;/p&gt;

&lt;p&gt;After deployment, every in-scope account contains a role with the same name, so the auditor can iterate over account IDs with a single ARN pattern:&lt;/p&gt;

&lt;p&gt;arn:aws:iam:::role/ExternalReadOnlyAuditRole&lt;/p&gt;

&lt;h2&gt;
  
  
  The auditor’s side
&lt;/h2&gt;

&lt;p&gt;From their own account, the auditor assumes the role:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws sts assume-role --role-arn arn:aws:iam::444455556666:role/ExternalReadOnlyAuditRole --role-session-name aws-audit  --external-id THE_EXTERNAL_ID&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For an organization audit, the auditor loops the same profile pattern over the account list — which they can fetch with &lt;code&gt;organizations:ListAccounts&lt;/code&gt;, included in the role’s permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ending the audit
&lt;/h2&gt;

&lt;p&gt;This is my favorite part, because it’s one line:&lt;/p&gt;

&lt;p&gt;aws cloudformation delete-stack --stack-name external-audit-access&lt;/p&gt;

&lt;p&gt;Delete the stack (or the org stack, which removes the StackSet instances from every member account). Access is gone. Nothing to rotate, nothing to deprovision, no forgotten IAM user with active keys waiting for the next auditor to find.&lt;/p&gt;

&lt;p&gt;The way you grant audit access says a lot about how you manage access everywhere else. If your next audit starts with “please create a user for us” — send them this article instead.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>devops</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>I tried Karpathy's LLM Wiki pattern on my AWS consulting work — here's what changed</title>
      <dc:creator>Alex </dc:creator>
      <pubDate>Tue, 14 Apr 2026 00:22:22 +0000</pubDate>
      <link>https://dev.to/upstood/i-tried-karpathys-llm-wiki-pattern-on-my-aws-consulting-work-heres-what-changed-571g</link>
      <guid>https://dev.to/upstood/i-tried-karpathys-llm-wiki-pattern-on-my-aws-consulting-work-heres-what-changed-571g</guid>
      <description>&lt;p&gt;I tried Karpathy's LLM Wiki principle on my own AWS consulting work a few weeks ago, and I can't go back.&lt;/p&gt;

&lt;p&gt;If you missed it, Andrej Karpathy published a short gist called &lt;strong&gt;&lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;LLM Wiki: A Pattern for Persistent Knowledge Bases&lt;/a&gt;&lt;/strong&gt;. His thesis: instead of using RAG to re-derive answers from raw sources on every query, let the LLM maintain a persistent, compounding wiki. Humans curate sources and ask questions. The model handles the bookkeeping — summarizing, cross-linking, keeping pages consistent over time.&lt;/p&gt;

&lt;p&gt;So I tried it. I rebuilt my entire AWS consulting knowledge base around Karpathy's three-layer pattern. This post is what I actually did, what the folder looks like now, the frontmatter schema that makes it queryable by Claude Code skills, and the three rules I had to enforce to stop it from rotting.&lt;/p&gt;

&lt;p&gt;At the bottom there's a copy-pasteable Claude Code prompt that scaffolds the same structure in an empty directory — if you want to try it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I tried this on AWS consulting specifically
&lt;/h2&gt;

&lt;p&gt;Every engagement I run has the same four information streams, and all four used to decay fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client-specific context&lt;/strong&gt; — their account structure, compliance posture, team, kickoff constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project AWS patterns&lt;/strong&gt; — cross-account alarm routing, IAM boundaries, cost allocation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decisions&lt;/strong&gt; — why I picked Aurora over RDS for this client, what tradeoff I accepted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gotchas&lt;/strong&gt; — what broke, in what AWS service, under what conditions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Before Karpathy's pattern, all four lived, well not in the same place let's say. Every new engagement brought the same conclusion that I was the bottleneck :).&lt;/p&gt;

&lt;p&gt;Now the LLM compiles it once and keeps it current for me. I'm not the bottleneck anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three layers, as I actually set them up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Layer 1 — Raw sources (immutable)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./raw/
├── assets/                      # Downloaded images, screenshots
├── aws-reinvent-2024-obs.md     # Clipped article
├── client-kickoff-2026-03.md    # Meeting transcript
├── datadog-quote-client-a.pdf   # Vendor docs
└── aws-bill-client-b-q1.csv     # Cost exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Raw never changes. If the wiki layer gets corrupted or I change conventions, I can re-derive from raw. That immutability is what gave me the confidence to let Claude aggressively refactor the wiki.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — Wiki (LLM-maintained markdown)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./wiki/
├── projects/
│   ├── client-a/
│   │   ├── _status.md             # Dashboard — I start here every session
│   │   ├── client-a-overview.md   # Architecture + key decisions
│   │   ├── decisions/             # ADR-style, one file per decision
│   │   ├── workflows/             # Automation-ready procedures
│   │   └── gotchas/               # What broke and why
│   └── client-b/
├── sa/                            # Solutions architecture patterns (cross-client)
├── aws/                           # AWS service patterns + gotchas
└── standards/                     # CDK conventions, tagging, account standards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One vault, per-client folders. Cross-client patterns live in &lt;code&gt;sa/&lt;/code&gt;, &lt;code&gt;aws/&lt;/code&gt;, &lt;code&gt;standards/&lt;/code&gt; — my opinion library. Client folders stay isolated so I never accidentally leak Client A's IAM choices into a Client B audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 — &lt;code&gt;CLAUDE.md&lt;/code&gt; (the schema)
&lt;/h3&gt;

&lt;p&gt;Karpathy calls this the "config for how the LLM operates on the wiki." For me, it's a single file at the vault root that defines the directory structure, the frontmatter schema, what belongs in the wiki and what doesn't, and the workflows I want Claude to follow (ingest a source, update &lt;code&gt;_status.md&lt;/code&gt;, lint for orphans).&lt;/p&gt;

&lt;p&gt;Change &lt;code&gt;CLAUDE.md&lt;/code&gt; and you change how every future Claude Code session operates on the vault. It's the load-bearing file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The frontmatter schema I landed on
&lt;/h2&gt;

&lt;p&gt;Every page has YAML frontmatter. Not for me to read — for skills to query:&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="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cross-account&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CloudWatch&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;alarm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;routing"&lt;/span&gt;
&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pattern&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="nv"&gt;aws&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cloudwatch&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;cross-account&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;observability&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;sources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;raw/aws-reinvent-2024-obs.md&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;client-a&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;span class="na"&gt;created&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-03-02&lt;/span&gt;
&lt;span class="na"&gt;updated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-04-11&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; field is the workhorse. Allowed values: &lt;code&gt;pattern | gotcha | decision | concept | process | status | workflow&lt;/code&gt;. When I run an audit skill, it reads only &lt;code&gt;type: gotcha&lt;/code&gt; and &lt;code&gt;type: decision&lt;/code&gt; pages — not the whole vault. That's the difference between fitting in the context window and blowing it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sources&lt;/code&gt; is non-negotiable for me. It keeps the agent honest when it synthesizes — every claim in the wiki traces back to something in raw.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first skill I wrote that made me a believer
&lt;/h2&gt;

&lt;p&gt;Here's the one that convinced me this was worth the effort: an &lt;code&gt;architecture-review&lt;/code&gt; skill I can run when a client proposes a change.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;architecture-review&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reviews a proposed architecture change against client constraints and cross-client patterns&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

Inputs:
&lt;span class="p"&gt;1.&lt;/span&gt; Client name (must match a folder in wiki/projects/)
&lt;span class="p"&gt;2.&lt;/span&gt; Proposed change (free text or linked diagram)

Steps:
&lt;span class="p"&gt;1.&lt;/span&gt; Read wiki/projects/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;/_status.md for current phase and constraints
&lt;span class="p"&gt;2.&lt;/span&gt; Read wiki/projects/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;-overview.md for architecture baseline
&lt;span class="p"&gt;3.&lt;/span&gt; Query wiki/sa/ and wiki/aws/ for pages where tags intersect with the change
&lt;span class="p"&gt;4.&lt;/span&gt; Query wiki/projects/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;/gotchas/ for anything relevant
&lt;span class="p"&gt;5.&lt;/span&gt; Query wiki/projects/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;/decisions/ for prior decisions that constrain this change
&lt;span class="p"&gt;6.&lt;/span&gt; Produce a review document with:
&lt;span class="p"&gt;   -&lt;/span&gt; Compatibility with existing architecture
&lt;span class="p"&gt;   -&lt;/span&gt; Relevant cross-client patterns that apply
&lt;span class="p"&gt;   -&lt;/span&gt; Prior decisions that would be affected
&lt;span class="p"&gt;   -&lt;/span&gt; Gotchas that could bite
&lt;span class="p"&gt;   -&lt;/span&gt; Recommended next steps

Output: wiki/projects/&lt;span class="nt"&gt;&amp;lt;client&amp;gt;&lt;/span&gt;/decisions/&lt;span class="nt"&gt;&amp;lt;YYYY-MM-DD&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;slug&amp;gt;&lt;/span&gt;.md (draft for human review)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no magic prompt here. The whole point is that the skill is running against &lt;strong&gt;structured, maintained context&lt;/strong&gt; — not a blank LLM. The quality of the output is about the quality of the wiki, not the prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three rules I had to enforce
&lt;/h2&gt;

&lt;p&gt;Karpathy warns that maintenance is what kills human wikis. Even with an LLM doing the work, I found three rules that actually matter in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Frontmatter or it didn't happen.&lt;/strong&gt; No frontmatter = skills can't index it. I make Claude add frontmatter before content on every new page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_status.md&lt;/code&gt; updates at session end, not start.&lt;/strong&gt; I set up a Claude Code Stop hook that blocks me from ending a session until the dashboard moves forward. Without the hook, dashboards went stale inside a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One page, one concept.&lt;/strong&gt; Gotchas get their own pages, not subheadings under a shared "troubleshooting.md". The retrieval unit is a page — keep it small enough that multiple fit in context without bleed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What actually changed in my work
&lt;/h2&gt;

&lt;p&gt;Three concrete things shifted after a few weeks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Architecture designs are regenerated, not rewritten.&lt;/strong&gt; A client constraint changes → I update the relevant page → rerun the design skill → new design doc. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audits are reproducible.&lt;/strong&gt; Same vault + same skill on the same day = same findings. I couldn't get that property out of ad-hoc Claude chats no matter how careful I was with prompts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handover is the deliverable.&lt;/strong&gt; At the end of an engagement, the client gets the CDK &lt;em&gt;and&lt;/em&gt; their vault folder. The vault &lt;em&gt;is&lt;/em&gt; the documentation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The output layer
&lt;/h2&gt;

&lt;p&gt;Everything Claude generates in my setup ends up as CDK. I open-sourced the constructs at &lt;strong&gt;up-cdk&lt;/strong&gt; — compliance-aligned building blocks for S3, VPC, pipelines, so every stack is compliant by default. Wiki + skills + opinionated output library is the combination that finally made the setup reproducible across clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try it — the bootstrap prompt
&lt;/h2&gt;

&lt;p&gt;The fastest way to see if this pattern works for you is to try it. Here's the Claude Code prompt I use to scaffold the whole structure in an empty directory. No scripts, no deps, just markdown and conventions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;You are setting up a new LLM Wiki following Andrej Karpathy's pattern
(https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f),
adapted for AWS consulting / platform engineering work.

The wiki has three layers:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`raw/`&lt;/span&gt; — immutable source material (meeting notes, clipped articles, bills, screenshots)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`wiki/`&lt;/span&gt; — LLM-maintained markdown synthesized from raw + conversation
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`CLAUDE.md`&lt;/span&gt; at the root — the schema document that tells you how to operate on the wiki

Create the following structure in the current directory. For every file below,
create it with the frontmatter and seed content described. Do not skip any
file. Do not invent additional files.

&lt;span class="gu"&gt;## Directory tree&lt;/span&gt;

./
├── CLAUDE.md
├── index.md
├── log.md
├── raw/
│   ├── assets/.gitkeep
│   └── README.md
├── templates/
│   ├── project-status.md
│   ├── page.md
│   └── workflow.md
└── wiki/
    ├── projects/
    │   └── _example-client/
    │       ├── _status.md
    │       ├── _example-client-overview.md
    │       ├── decisions/.gitkeep
    │       ├── workflows/.gitkeep
    │       ├── gotchas/.gitkeep
    │       └── diagrams/.gitkeep
    ├── sa/.gitkeep
    ├── aws/.gitkeep
    └── standards/.gitkeep

&lt;span class="gu"&gt;## CLAUDE.md (the schema document)&lt;/span&gt;

Write a root CLAUDE.md that defines:
&lt;span class="p"&gt;-&lt;/span&gt; Purpose: personal AWS knowledge base following Karpathy's LLM Wiki pattern.
&lt;span class="p"&gt;-&lt;/span&gt; The three layers: raw/ (immutable), wiki/ (LLM-generated), CLAUDE.md (schema).
&lt;span class="p"&gt;-&lt;/span&gt; What belongs in the wiki: durable knowledge (gotchas, decisions, patterns,
  strategy, workflows, status dashboards). What does NOT belong: raw doc
  summaries, git state, ephemeral task context.
&lt;span class="p"&gt;-&lt;/span&gt; Page format — every wiki page must have YAML frontmatter with fields:
  title, type (pattern | gotcha | decision | concept | process | status |
  workflow), tags, sources, created, updated.
&lt;span class="p"&gt;-&lt;/span&gt; File naming — kebab-case. Overview files: &lt;span class="nt"&gt;&amp;lt;project&amp;gt;&lt;/span&gt;-overview.md. Status
  files: _status.md (underscore prefix sorts to top).
&lt;span class="p"&gt;-&lt;/span&gt; Cross-references — [[filename]] Obsidian-style wiki links.
&lt;span class="p"&gt;-&lt;/span&gt; Every page ends with a ## Related section.
&lt;span class="p"&gt;-&lt;/span&gt; Workflows (document all four):
&lt;span class="p"&gt;  1.&lt;/span&gt; Quick context recovery at session start — read
     wiki/projects/&lt;span class="nt"&gt;&amp;lt;project&amp;gt;&lt;/span&gt;/_status.md first.
&lt;span class="p"&gt;  2.&lt;/span&gt; Update _status.md at session end — refresh Last/Next session lines,
     append one row to Session History (keep last 5), update Recent Decisions
     and Open Questions, set updated: to today.
&lt;span class="p"&gt;  3.&lt;/span&gt; Ingest a raw source — read thoroughly, write/update pages in the right
     wiki/ subdirectory, cross-link, update index.md, append to log.md.
&lt;span class="p"&gt;  4.&lt;/span&gt; Lint — find orphan pages, stale content, missing back-links.
&lt;span class="p"&gt;-&lt;/span&gt; Common tags taxonomy — starter list for AWS, SA, meta tags.
&lt;span class="p"&gt;-&lt;/span&gt; Persistence layer rules — wiki is the single source of truth for durable
  knowledge.

&lt;span class="gu"&gt;## index.md&lt;/span&gt;
A one-page catalog. Headers for each top-level wiki directory (## Projects,
&lt;span class="gu"&gt;## Solutions Architecture, ## AWS, ## Standards), each with "No pages yet."&lt;/span&gt;

&lt;span class="gu"&gt;## log.md&lt;/span&gt;
Append-only activity log. Seed with today's date and "Initialized wiki
following Karpathy LLM Wiki pattern."

&lt;span class="gu"&gt;## templates/project-status.md&lt;/span&gt;
Template for _status.md files. type: status. Sections: Phase, Last session,
Next session, Current State, Recent Decisions, Open Questions, Session
History (Date | Summary table), Quick Links.

&lt;span class="gu"&gt;## templates/page.md&lt;/span&gt;
Generic page template. Frontmatter + ## Key Points / ## Details / ## Related
/ ## Sources.

&lt;span class="gu"&gt;## templates/workflow.md&lt;/span&gt;
Automation-ready procedure template. type: workflow. Sections: Goal,
Prerequisites, Steps (numbered, imperative), Verification, Rollback (if
applicable), Related.

&lt;span class="gu"&gt;## wiki/projects/_example-client/_status.md&lt;/span&gt;
Realistic example status dashboard. Phase: active. Plausible placeholder
content. Marked as example.

&lt;span class="gu"&gt;## wiki/projects/_example-client/_example-client-overview.md&lt;/span&gt;
Example architecture overview. Frontmatter + 3-sentence overview + Key
Points + Architecture + Recent Decisions + Related.

&lt;span class="gu"&gt;## raw/README.md&lt;/span&gt;
One-page explanation of what goes in raw/ and the immutability rule: never
edit raw files after adding them.
&lt;span class="p"&gt;
---
&lt;/span&gt;
After creating everything, print a summary (file count + tree) and next
steps: (1) add your first raw source to raw/, (2) create your first real
project folder under wiki/projects/, (3) commit to git.

Do not add files beyond what's specified. Do not add a root README.md.
Do not add a LICENSE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it from an empty directory, then &lt;code&gt;git init&lt;/code&gt;, then add your first raw source. I had a working LLM Wiki in under two minutes.&lt;/p&gt;

&lt;p&gt;The original gist is short and worth reading: &lt;a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f" rel="noopener noreferrer"&gt;Karpathy — LLM Wiki&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you try it, drop a comment with how you ended up shaping it. I'd genuinely like to compare notes.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>documentation</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
