Where Your Secrets Really Live: From IaC State to a Running Container

The credential leaks that actually make it into a postmortem tend to follow the same shape. A secret that was supposed to exist in exactly one place ends up existing in four: a .tfvars file someone forgot to delete, a CI log that echoed an environment variable during a failed-deploy debugging session, a state file with the value sitting in plaintext because Terraform needed it to create a resource, and the running container, which is the only place it was actually meant to be.

Secrets Management!

None of the tooling in this space is really about generating or storing a string securely. That part’s not hard. What’s hard is controlling how many places that string touches on its way from “created” to “used by a workload,” and closing off every one of those places except the last.

The map

A secret’s actual path through a modern deployment crosses four systems, and each one leaks it in its own particular way: the infrastructure-as-code tool that provisions the store, the CI/CD or GitOps engine that ships the workload, the store itself, and the runtime platform the workload runs on.

Secret Management Flow

Almost every incident I’ve dug into traces back to one of those four boundaries being noticeably weaker than the other three. Hardening one while ignoring the rest doesn’t close the leak, it just relocates it.

Infrastructure as code: the tool that provisions the vault, imperfectly

CloudFormation and Terraform dominate this layer for AWS-centric shops, and they fail in genuinely different ways.

CloudFormation’s answer is dynamic references: a string like {{resolve:secretsmanager:secret-id:SecretString:json-key}} that CloudFormation resolves at deploy time instead of you typing a password into a template. Pair that with the NoEcho attribute on the parameter and the value won’t print in the console or in describe-stacks. It’s a solid pattern with one sharp edge: NoEcho hides the value, it doesn’t encrypt it, and it offers no protection against someone who already has stack-read permissions. CloudFormation also won’t resolve a dynamic reference inside resource metadata like AWS::CloudFormation::Init, because that would print the secret straight to the console anyway. It’s a small but telling detail about where the real exposure risk actually sits. One advantage CloudFormation has by default: there’s no separate state file for you to secure, because the state lives inside the service.

Terraform’s version of this problem is structural rather than incidental. Mark a variable sensitive = true and Terraform redacts it from your terminal, your plan output, and your CI logs. It does nothing to the state file. Any sensitive value that becomes a resource attribute — a database password, an environment variable on a Lambda function — lands in terraform.tfstate in plain text regardless of how you flagged it, because Terraform needs the real value there to detect drift. Explaining that distinction to a team that assumed the sensitive flag was doing more than it actually does is a conversation most Terraform users eventually have.

For years the honest answer was: encrypt your state backend, restrict who can read it, and treat the state file itself as sensitive data. That’s still correct, but it’s no longer the whole story. Terraform 1.10 introduced ephemeral values, and 1.11 extended that to write-only arguments on managed resources, and together they’re an actual fix rather than a mitigation. An ephemeral resource fetches a value (a secret from Vault, a freshly generated password) and Terraform uses it during the apply without ever writing it to plan or state. A write-only argument, password_wo instead of password, accepts that ephemeral value directly on a resource like aws_db_instance, and the value is gone the moment the apply finishes. If you’re standing up new Terraform configurations this year, there’s not much reason to keep pulling secrets through a data source and hoping your state encryption is strong enough. The one thing worth planning for: migrating an existing attribute to its _wo form can trigger a resource replacement depending on the provider, so treat that migration like a credential rotation and test it somewhere that isn’t production.

CI/CD and GitOps are solving two different secret problems

People use “CI/CD” and “GitOps” almost interchangeably, but they hand secrets to infrastructure in opposite directions, and that difference matters more than which specific tool you’ve standardized on.

A CI/CD pipeline (GitHub Actions, Jenkins, Azure DevOps, or Spacelift running your Terraform) is push-based. Something happens, a runner spins up somewhere outside your infrastructure, that runner needs credentials to reach into your cloud account and your secret store, and then it pushes a change. The secret problem here is entirely about the runner: how does this ephemeral, often shared compute prove who it is without you handing it a long-lived key it might leak in a stray log line?

GitOps (Argo CD, Flux) is pull-based. An agent already lives inside the cluster and continuously reconciles actual state against what’s declared in Git. There’s no discrete pipeline-run moment to inject a secret from outside; the reconciler has to fetch it itself, from inside the cluster, and Git can never hold the plaintext value because Git is exactly where everyone with repo access can see it. That’s a different problem than “authenticate a runner,” which is why GitOps secret tooling looks nothing like CI/CD secret tooling.

On the CI/CD side, most of the industry has converged on the same answer: stop handing runners long-lived cloud credentials at all. GitHub Actions federates over OIDC: the workflow requests a short-lived token from GitHub’s own identity provider, and an IAM role’s trust policy accepts that token only from specific repositories, branches, or environments. No access key sits in GitHub Secrets waiting to be exfiltrated by a compromised dependency. Azure DevOps gets most of the way there with federated service connections, though the pattern I still see most often is a variable group linked to an Azure Key Vault: the group stores secret names rather than values and pulls the live value from the vault at queue time, so a vault outage fails the run early instead of mid-deploy. One thing that catches people off guard the first time: a variable group can’t link to a Key Vault that uses Azure RBAC for its permission model — the vault has to be running the older access-policy model instead. If secrets change often enough that a queue-time pull isn’t fresh, the AzureKeyVault@2 task reads the vault mid-run instead, at the cost of an extra step in every pipeline.

Jenkins is the odd one out because it predates all of this, and the right way to hand it secrets depends entirely on where the agents actually run. Agents on Kubernetes should use Vault’s Kubernetes auth method and let the pod’s own service account token do the authenticating. Agents on long-lived VMs typically use AppRole instead. The Vault plugin for Jenkins handles the retrieval and masks secrets in the build log, but “the plugin masks it” is a claim worth being a little skeptical of. An advisory against the plugin found that masking silently failed for secrets printed from shell steps on an agent when a specific durable-task logging mode was enabled; a workaround shipped in a related credentials plugin, but the underlying issue was still open at the time the advisory was published. The lesson generalizes past this one CVE: log masking is a convenience feature, not a security boundary, and you should assume that anything a build step can print, eventually it will.

Spacelift, which is closer to purpose-built CI/CD for Terraform than a general-purpose runner, handles this with contexts: reusable bundles of environment variables and mounted files attached to one or many stacks. Mark a variable or a mounted file as secret and it’s hidden from the UI and the API, visible only to the run itself. It composes nicely with Terraform’s own write-only arguments — Spacelift keeps the value out of its dashboard, Terraform keeps it out of state, and neither tool has to do the other’s job.

On the GitOps side, three patterns cover almost every cluster I’ve come across, and they trade off differently enough that picking one is a real decision rather than a coin flip.

ApproachWhere the secret livesRotationBest fit
Sealed SecretsEncrypted in Git, decrypted only by an in-cluster controllerManual, no external store to pollSmall teams with no existing secret store, comfortable with cluster-key lock-in
SOPSEncrypted in Git via KMS or ageManual re-encryption on changeConfig that spans Kubernetes and non-Kubernetes systems
External Secrets OperatorReferenced in Git; the real value stays in Vault, Secrets Manager, or Key VaultAutomatic, on a sync intervalTeams that already run a real secret store and want rotation to just work

I’ve moved teams off Sealed Secrets more than once, and it’s rarely about the encryption being weak. It’s that the cluster’s private key becomes a single point of failure people forget about until they’re migrating clusters and discover nothing sealed two years ago can be unsealed anywhere else. External Secrets Operator avoids that by keeping Git as a set of pointers instead of a vault substitute, which is honestly the more defensible GitOps posture: Git should describe which secret a workload needs, not contain it in any form.

The stores themselves: what you’re actually paying for

AWS Secrets Manager, Azure Key Vault, and HashiCorp Vault all do the core job (encrypted storage, access control, an audit trail) well enough that the choice usually comes down to which cloud you’re already committed to, plus one real conceptual difference.

Secrets Manager and Key Vault are, underneath the branding, secured key-value stores with a rotation feature bolted on. That’s not a criticism. For a team running in one cloud, storing mostly static credentials and rotating them on a schedule, that’s exactly the right amount of tool, and you get IAM or Entra ID integration for free instead of standing up a separate identity story.

Vault’s dynamic secrets are the actual differentiator, and they change the security model rather than just the storage location. Instead of storing a long-lived database password and rotating it on a schedule, Vault’s database engine creates a brand-new database user with a short lease every time a workload asks for one, and revokes it the moment the lease expires or the workload shuts down. Most of the time, there is no standing credential to steal, because most of the time there isn’t a credential at all. That’s a genuinely stronger posture, but it comes with real operational weight: an engine to configure per database type, lease-renewal logic your applications need to handle, and a Vault cluster that now needs its own high availability, because your workloads can’t start without it. I’d only take that on if you’re actually going to use dynamic secrets for something: databases, cloud IAM, PKI certificates. If you’re going to end up storing mostly static values and checking a rotation box, you’ve built a more complicated Secrets Manager, not a better one.

Getting the secret into a running container

This is where the container platform’s own identity model decides how much of the pain above you actually feel.

ECS keeps it simple by splitting responsibility across two IAM roles that get conflated constantly the first few times someone sets this up: the execution role, which the ECS agent assumes to pull the image from ECR and to resolve any secrets block in the task definition (calling Secrets Manager or SSM Parameter Store before the container even starts), and the task role, which the application itself assumes at runtime for everything else. Put the secret permission on the task role instead of the execution role and the container just fails to start, with an error message that doesn’t always point at which role is missing what.

EKS and AKS have more moving parts because Kubernetes wasn’t originally built with cloud IAM in mind, so there’s a federation step before you even reach the secret. On EKS, that step used to mean IRSA exclusively (mapping a Kubernetes service account to an IAM role through the cluster’s OIDC provider), and IRSA still works and remains the practical choice for Fargate and Windows node groups. But EKS Pod Identity, generally available since late 2023, has become the default recommendation for new EC2-based clusters: no per-cluster OIDC provider to wire up, a universal trust policy instead of one keyed to each cluster’s issuer URL, and IAM session tags for namespace and service account applied automatically. Once that identity exists, the actual secret retrieval is usually External Secrets Operator syncing a value from Secrets Manager or Vault into a native Kubernetes Secret on an interval, or the Secrets Store CSI driver mounting it as a volume without ever materializing a Kubernetes Secret object. That’s a real difference if you’re trying to keep secrets out of etcd entirely. AKS follows the same shape with its own managed identity and CSI driver combination, including autorotation that polls Key Vault every two minutes by default and updates the mounted file and any synced Secret without a pod restart.

If you’re running EKS on mostly EC2 node groups and still wiring up IRSA for new workloads out of habit, that’s worth a second look. Pod Identity isn’t dramatically more secure by itself, but it removes an entire category of trust-policy maintenance that scales badly past one or two clusters.

Rotation: the step most projects only half-finish

Storing a secret securely and rotating it are different problems, and plenty of “secrets management” initiatives quietly solve only the first one.

AWS Secrets Manager’s rotation runs through a Lambda function that Secrets Manager calls in four steps on a schedule, and the mechanics explain why it can be close to zero-downtime instead of a coordinated cutover.

AWS Secret Rotation

Secrets Manager tracks versions with staging labels instead of overwriting anything in place. AWSCURRENT is whatever your application is using right now. A new version is created and labeled AWSPENDING while createSecret and setSecret do the actual work of generating a new credential and pushing it to the database or service, and only once testSecret confirms the new credential logs in does finishSecret flip the labels: AWSPENDING becomes the new AWSCURRENT, and the old current version becomes AWSPREVIOUS. Nothing reading the secret mid-rotation ever sees a half-updated value, because the label that matters doesn’t move until the new credential is proven to work.

Vault handles rotation by mostly avoiding the need for it. Dynamic secrets carry a short lease instead of a rotation schedule, so “rotation” for a database credential is really Vault refusing to hand out a lease longer than its TTL and issuing a fresh one on the next request. That’s arguably the cleaner model, but it only covers secrets Vault generates itself; anything stored as a static key-value pair rotates exactly as manually as it would anywhere else.

The IaC layer has its own rotation trap, and both major tools share it. CloudFormation’s dynamic references and Terraform’s data-source lookups both resolve a secret’s value once, at deploy time, and neither one watches the store for changes afterward. Pin a CloudFormation dynamic reference to a specific version-id and a background rotation in Secrets Manager will never touch your stack, not until you push a template change that touches the resource. That’s exactly why AWS’s own guidance is to use versionless references, so a stack update always picks up whatever AWSCURRENT happens to be. It’s a small detail that’s generated more than one confused ticket asking why a rotation “didn’t take.”

Hardening: shrinking the blast radius

Everything above assumes the access paths are already reasonably tight. Hardening is about making sure a compromised credential, a leaked log, or an overprivileged role can’t turn into anything worse than it already is.

Network path matters more than people give it credit for. Secrets Manager supports a VPC interface endpoint, and once that endpoint exists, a secret’s resource policy can add an aws:SourceVpce condition that denies any request not arriving through it, collapsing the attack surface from “anyone with the right IAM permissions from anywhere” down to “traffic that physically transited this one endpoint inside the VPC.” AWS also recommends BlockPublicPolicy: true on any identity allowed to attach resource policies, which uses AWS’s own automated reasoning to reject a resource policy granting broad or public access before it ever takes effect, rather than relying on someone catching it in review.

Identity is the other half of this. Every federation pattern described above, and every IRSA or Pod Identity association, exists for the same reason: a short-lived, narrowly scoped credential that expires on its own beats a long-lived one that has to be remembered, stored, and eventually rotated by a person. A workload or pipeline still authenticating with a static access key at this point is usually not a deliberate choice, it’s just the oldest unresolved thing in the account.

And then there’s the secret that never should have left a laptop. Gitleaks and TruffleHog cover this from two angles. Gitleaks runs fast, offline pattern matching that’s cheap enough as a pre-commit hook to block a leak before it’s even recorded in history. TruffleHog goes further and verifies whether a detected credential is still live with a real, read-only call against the provider it belongs to, which matters when triaging which of the dozens of things a full history scan just turned up are actual emergencies. Running both, one at commit time and one on a schedule against full history, catches more than either alone would. It’s worth taking seriously specifically because of how coding assistants have changed the failure mode: a GitGuardian report earlier this year found AI-assisted commits leaking secrets at roughly double the rate of human-typed ones, which tracks. Pasting a working credential into a prompt or a generated config file is a much easier way to leak one than typing it from memory ever was.

None of these controls substitute for each other. Scoping a secret to a VPC endpoint does nothing if the IAM role allowed to read it is wide open. Rotating on a schedule doesn’t help if the previous version is still sitting in three CI logs somewhere. The actual work is closing every path on the map from the start of this post, not just the one that happens to look most broken today.

If there’s one rule that generalizes

Count the number of places a secret’s plaintext value could theoretically be read by something other than the workload that needs it: a state file, a build log, a Kubernetes Secret sitting in etcd, a debug terraform output, a .env pasted into a chat message. Every tool in this post is, underneath its specific syntax, either reducing that count or increasing it. Write-only arguments reduce it. A Sealed Secret sitting in a public repo doesn’t touch it at all, which is the point. A sensitive = true flag that a team believes is doing more than it actually is increases it, quietly, until someone finds out the hard way. Pick tools by asking which direction they move that number, and most of the decisions in this post get easier to make on their own.

VPC Lattice: What It Actually Replaces, What It Costs, and When I’d Reach for It

AWS App Mesh shuts down on September 30, 2026. Not “enters maintenance mode” — the console and the resources stop working. If you’re one of the teams that bet on it in 2019 for EKS service-to-service traffic, you’ve got a couple of months left, and AWS has been pointing everyone at the same replacement for ECS workloads and EKS workloads alike: Amazon VPC Lattice.

That deadline is a decent excuse to actually understand what Lattice is, because it’s not just an App Mesh clone. It quietly replaces a chunk of what Transit Gateway and PrivateLink do too, and it changes some assumptions network engineers have had baked in since VPC peering existed. Worth twenty minutes even if you have no App Mesh to migrate.

What VPC Lattice actually is

Strip away the marketing and VPC Lattice is a managed, regional application-layer proxy that sits between your consumers and your services, handles service discovery, applies IAM-based authorization, and routes requests — all without you deploying a load balancer, a sidecar, or a peering connection. AWS’s own description is that it manages network connectivity and application layer routing between services across different VPCs and AWS accounts, and that’s a fair summary, if a little dry.

The mental model that clicked for me: think of it as a load balancer that doesn’t live in any one VPC. You publish a service into a service network (a logical boundary, not a piece of infrastructure you provision), and any VPC or account associated with that service network can reach it by DNS name — no route tables, no CIDR planning, no peering mesh growing into an unmanageable tangle.

Lattice Service Network.

A service network can span accounts via AWS Resource Access Manager, so a platform team can own the network and share it out to application teams without those teams ever touching a route table. That’s the part that made this interesting to me as an architect rather than as a networking hobbyist — it moves network topology out of the app team’s problem space entirely.

What it’s quietly replacing

Nobody built this in a vacuum. Every one of these had a gap Lattice was built to close.

VPC peering doesn’t scale past a certain account count — it’s point-to-point, so N accounts means something close to N² connections, and overlapping CIDRs break it outright. Lattice’s data plane assigns consumers a link-local address and doesn’t care what your VPC CIDR looks like, overlapping or not.

Transit Gateway solved the peering-mesh problem at layer 3/4 — it’s still the right tool for routing IP traffic between VPCs and on-premises networks at scale, and I wouldn’t rip one out to replace it with Lattice. But TGW has no concept of an HTTP request, a path, or an identity. It moves packets, not requests. If your actual problem is “service A needs to call /v2/orders on service B and I want to enforce that with IAM,” TGW can’t help you get there on its own.

AWS PrivateLink is the closest sibling — it’s also a proxy-based, non-peering way to expose a service across accounts. The difference is PrivateLink is fundamentally point-to-point: each consumer VPC needs its own interface endpoint, and there’s no native request-level routing or IAM authorization baked into the data path. Lattice centralizes that into one service network that many consumers attach to, and layers on HTTP-aware routing rules on top.

AWS App Mesh is the one with the deadline. It gave you real service-mesh features — client-side retries, circuit breaking, fine control — but it did that with an Envoy sidecar next to every task, which is real operational weight: certificate rotation, sidecar upgrades, resource overhead per pod. Lattice deliberately drops the sidecar. You lose some of App Mesh’s client-side sophistication in the trade — Lattice’s routing and auth decisions happen server-side, not in a proxy next to your code — but for most teams that’s a fair trade for not operating a mesh control plane.

VPC PeeringTransit GatewayPrivateLinkVPC Lattice
OSI layerL3L3/L4L3/L4L7 (app layer)
Cross-account model1:1 meshHub-and-spoke1:1 endpoint per consumerMany-to-many via service network
CIDR overlap tolerantNoNoYesYes
Identity-aware authNoNoNo (SG/NACL only)Yes (IAM/SigV4)
Request-level routingNoNoNoYes (path, method, header, weighted)
Sidecars requiredNoNoNoNo

How it’s actually built

Four concepts do the work. A service is the logical front door — listeners, rules, target groups — conceptually a load balancer, but one that isn’t tied to a single VPC. A listener is a protocol and port (HTTP, HTTPS, or TLS_PASSTHROUGH). A rule matches on path, HTTP method, or header and forwards to a target group, which can hold EC2 instances, IP addresses, Lambda functions, or an Application Load Balancer, and — as of the more recent releases — ECS and Fargate tasks natively alongside EC2 and EKS.

Worth calling out explicitly: Lattice terminates HTTPS itself using an ACM-managed certificate, which is convenient, but it only does server-side TLS. If you need mutual TLS, you have to use a TLS_PASSTHROUGH listener and let the target negotiate the client certificate — Lattice’s own data plane won’t do mTLS termination for you.

The newer half of the story is VPC Resources and Resource Gateways, which let you expose non-HTTP TCP endpoints — a database, an internal domain name, an on-premises system reachable over Direct Connect or VPN — through the same service network model, complete with the same IAM controls. This is the part that pushes Lattice past “App Mesh replacement” and into “how do I expose an RDS instance to another account without a peering connection or a bastion.” It’s genuinely useful, and it’s the feature I’d bring up first if a platform team asked me why this deserves attention beyond the App Mesh deadline.

For Kubernetes specifically, the AWS Gateway API Controller implements the open-source Kubernetes Gateway API and translates Gateway and HTTPRoute objects into Lattice service network objects behind the scenes. If your EKS team already writes Gateway API manifests, adopting Lattice barely changes their workflow — it’s the controller that’s doing the AWS-specific work.

Authorization runs on IAM. Auth policies attached at the service network or service level use standard IAM policy JSON, and clients authenticate with SigV4-signed requests, the same signing scheme every other AWS API call uses. This is a genuine strength if your org already centers identity around IAM roles, and a genuine adoption cost if you have legacy clients that have never had to sign a request in their life. Budget time for that conversation — it comes up in almost every migration writeup I’ve read.

When I’d actually reach for it

When Lattice makes sense.

Lattice earns its place when the actual requirement is cross-account or cross-VPC service exposure with identity-aware access control and some request-level routing, and you don’t want to own a control plane to get it. New microservice builds, platform teams centralizing how application teams expose services to each other, EKS shops trying to get off App Mesh before the clock runs out, and anyone who’s been maintaining a PrivateLink endpoint-per-consumer sprawl are the clearest fits.

I’d think twice before using it as a wholesale Transit Gateway replacement. It’s not built for raw IP-layer routing at TGW’s scale, and — as I’ll get to in pricing — the per-service hourly charge adds up fast if you’re running hundreds of services with modest traffic each. I’d also pause if you need genuine client-side mesh behavior: retries with backoff, circuit breakers, fault injection for chaos testing. That logic sits server-side in Lattice, which is simpler to operate but less flexible than a sidecar that runs next to your code.

Pros and cons, plainly

The upside is real: no sidecars to patch, no peering mesh to keep untangled, IAM auth you already understand, weighted routing for blue/green and canary out of the box, and a service directory that gives you an actual inventory of what’s exposed to what. Observability is baked in too — access logs and CloudWatch metrics per service without instrumenting anything yourself.

The downside is mostly about maturity and rigidity. It’s newer than TGW or PrivateLink, so you’ll hit rough edges — G2 reviewers flag protocol support currently limited to HTTP, HTTPS, and gRPC, and region coverage, while it’s grown a lot since GA, still isn’t everywhere. Security group design has a real gotcha: targets need to allow inbound traffic from the Lattice association security group, not from the consumer’s VPC CIDR, and that trips people up in early deployments because it looks wrong at first glance. And moving to IAM/SigV4 auth is a genuine client-side change — nothing you can skip past.

Pricing — the part that actually decides adoption

Three dimensions drive the bill: an hourly charge per service, a per-GB data processing charge, and a per-request (or per-connection, for TLS_PASSTHROUGH) charge above a free tier. In US East (N. Virginia), that’s $0.025 per service-hour, $0.025 per GB processed, and $0.10 per million requests beyond the first 300,000 free per hour. Compare that to PrivateLink at roughly $0.01 per hour per AZ plus $0.01/GB with volume discounts, and it’s clear Lattice costs more per unit — you’re paying for the extra L7 intelligence and the simpler operating model, not for cheaper bytes.

Run the numbers on a single, modestly busy service and it’s not scary: one service processing 100 GB and 200,000 requests an hour for a month lands around $18–20 in hourly charges alone before data and requests, and a heavier example AWS publishes — one service with HTTPS and TLS listeners together processing 2,100 GB and millions of requests a month — comes out to roughly $268 a month. The bill gets serious at fleet scale. One cloud architect’s published comparison modeled 200 services across 100 accounts pushing 30TB a month: Transit Gateway came out around $4,250, Lattice around $4,840 — roughly a 14% premium, driven almost entirely by the per-service hourly charge rather than data processing, which was actually cheaper for TGW in that scenario. If you’re running that many low-traffic services, the fixed hourly cost per service matters more than the per-GB rate.

VPC Resources (the database/on-prem exposure feature) bill separately and use a tiered per-GB rate: $0.01/GB for the first petabyte in a Region each month, dropping to $0.006 and then $0.004/GB at higher volumes, plus a small hourly charge per resource. Worth modeling separately from your service traffic if you’re planning to route a lot of data through it.

Quotas worth knowing before you design around them

These are the ones that actually shape an architecture, not just trivia:

QuotaDefaultAdjustable
Services per Region2,000Yes
Service networks per Region50Yes
Service associations per service network500Yes
VPC associations per service network500Yes
Target groups per service10Yes
Targets per target group1,000Yes
Listeners per service2Yes
Rules per listener10Yes
Requests/sec per service per AZ10,000Contact your SA/TAM
Bandwidth per service per AZ10 GbpsContact your SA/TAM
Connection idle timeout (HTTP/gRPC)1 minuteContact your SA/TAM
Max connection lifetime10 minutesFixed
Service networks a VPC can associate with directly1— (use service-network VPC endpoints for more)

Full, current numbers are on the official quotas page — check it before you design, not after you hit a wall. The one that catches people off guard most is the one-service-network-per-VPC-via-direct-association limit; if you need a VPC in more than one service network, you’re routing through service-network VPC endpoints instead, which is an extra layer to plan for.

A few concrete use cases

A platform team centralizing how forty application teams expose internal APIs to each other, replacing forty sets of ad-hoc security-group rules and a slowly decaying peering mesh with one service network and a consistent IAM auth policy. A company running canary deployments where 5% of production traffic gets weighted onto a new service version before a full cutover — Lattice’s weighted target groups do this natively, no custom load balancer logic required. An EKS shop migrating off App Mesh before the September deadline, using the Gateway API Controller so app teams keep writing the same HTTPRoute manifests they already know. And the resource-gateway pattern: exposing a shared RDS instance in a data-platform account to a dozen consuming accounts without provisioning a PrivateLink endpoint in every one of them.

Why this belongs on your radar even without the App Mesh deadline

The App Mesh shutdown is the forcing function, but the more interesting shift is architectural: AWS is pulling network topology out of individual VPCs and into a service-oriented abstraction that’s owned centrally and consumed by reference. That’s a meaningful change to how you’d write a network architecture standard or review an ADR — the question stops being “how do these two VPCs route to each other” and starts being “which service network does this belong to, and what’s the IAM policy governing who can call it.” If you own architecture review or network standards for your org, that’s worth getting ahead of before it shows up in someone else’s design doc and you’re reviewing it cold.

It’s not a wholesale replacement for Transit Gateway, and it’s not free — budget the per-service hourly charge honestly before you commit a few hundred services to it. But for the specific problem it targets, cross-account and cross-VPC service exposure with identity-aware access control, it’s a cleaner answer than anything that came before it, and worth prototyping now rather than in month eleven of an App Mesh migration deadline.

AWS Landing Zone, Part 3: Transit Gateway, Centralized Egress, and Where IPAM Earns Its Keep

Five VPCs connected by peering need ten peering connections. Ten VPCs need forty-five. Every new VPC means going back into the route tables of every existing VPC to add the new relationship, and every peering connection is a distinct thing that can be misconfigured, forgotten, or left open longer than it should be. That math is the whole argument for hub-and-spoke, and it’s why almost nobody designs a multi-account AWS network as a full peering mesh past the first few accounts.

AWS Landing Zone – Part 3

Part 1 and Part 2 covered the account structure and the guardrails that govern it. This post is about the network underneath: Transit Gateway as the hub almost everyone reaches for, centralized egress and what it costs against the alternative, IPAM (IP Address Management) as the CIDR governance layer, and where VPC Lattice fits without pretending it replaces Transit Gateway wholesale.

Transit Gateway as the hub

AWS Transit Gateway is a managed, regional routing hub. Spokes, VPCs, VPN connections, Direct Connect, attach to it once, and it handles routing between them using its own route tables instead of point-to-point relationships. One Transit Gateway per Region is typically enough since it’s highly available by design, though there’s a legitimate case for more than one where you want to limit the blast radius of a routing misconfiguration or separate control-plane operations between teams. Cross-region connectivity happens through Transit Gateway peering; hybrid connectivity, Direct Connect or VPN, attaches the same way a VPC would.

The account this lives in matters. Transit Gateway, IPAM, and centralized egress infrastructure all belong in the Infrastructure OU’s networking account, sometimes literally called Network or Network Hub, not scattered across workload accounts and not in the Control Tower management account. That account becomes the place a network engineer actually works day to day, and it’s worth treating its own access and change process with the same care as the Security OU from Part 2.

Laid out, the topology looks like this:

Transit Gateway topology!

Every spoke VPC gets one attachment to the hub instead of a direct relationship with every other spoke. Whether Prod and Test can actually reach each other is a routing table decision inside the Transit Gateway, not something the topology forces one way or the other, which is exactly the control you lose with a flat peering mesh.

Centralized egress and what it actually costs

The other big reason to centralize on a hub isn’t just routing hygiene, it’s cost. Every VPC that needs internet access wants its own NAT gateway per Availability Zone for resilience, and NAT Gateway data processing charges add up fast once you’re running dozens of VPCs, each with its own pair or trio of gateways running independently. Centralizing egress means routing outbound traffic from every spoke through the Transit Gateway into a single egress VPC in the network account, where it exits through one set of NAT gateways instead of dozens. One practitioner audit found organizations spending around $15,000 a month on NAT Gateway data processing alone, spread thin across VPC after VPC, and cut that by 40 to 70 percent by consolidating.

Adding traffic inspection to that same choke point is a natural next step. AWS Network Firewall sits in the egress VPC and inspects traffic before it reaches the NAT gateway, at a real but bounded cost, roughly $0.40 an hour per endpoint plus a per-gigabyte processing charge, which is a fair trade in a regulated environment and a harder sell if you’re mostly optimizing for spend. One genuine gotcha here: DNS doesn’t follow this path by default. Route 53 Resolver and DNS Firewall are a separate egress route entirely, so centralizing your data-plane egress through Transit Gateway and Network Firewall doesn’t automatically mean your DNS queries are inspected the same way. If DNS-based filtering matters to your threat model, it needs its own explicit design, not an assumption that it rides along with everything else.

IPAM: get the CIDR plan right before account one

IP address planning is one of those things that’s cheap to fix before it exists and expensive to fix after. Amazon VPC IPAM gives you a hierarchical pool structure, a top-level pool subdivided into regional pools, then further into business-unit or environment pools, so that a new VPC in the Workloads_Test OU pulls its CIDR from a pool already guaranteed not to overlap with Prod, Sandbox, or anything else in the organization. IPAM should be delegated to the network account rather than run from the Control Tower management account, the same separation-of-duties instinct as everything else in this series.

The failure mode IPAM prevents is duller than a security incident but just as disruptive: two teams independently pick 10.0.0.0/16 for their VPCs, everything works fine in isolation, and then someone needs to connect the two networks and discovers the ranges collide. Fixing that after the fact means readdressing a live VPC, which is exactly the kind of maintenance window nobody wants to schedule. Getting the CIDR plan right before the first account exists costs an afternoon. Getting it wrong costs a migration.

Route 53 Profiles solve the equivalent problem for DNS. Instead of manually associating private hosted zones, resolver rules, and DNS firewall rule groups to every VPC individually, you bundle them into a single profile in the network account and share it across accounts through AWS RAM. New VPCs associate with the profile once and inherit the whole DNS configuration, rather than someone remembering to wire up each piece by hand every time an account gets vended.

Where VPC Lattice actually fits

VPC Lattice gets pitched sometimes as a Transit Gateway replacement, and that framing oversells it. Transit Gateway operates at Layer 3, it moves packets between IP addresses and doesn’t know or care which identity sent them; security has to come from route tables, security groups, and NACLs. VPC Lattice operates at Layer 7, HTTP, HTTPS, and gRPC specifically, and it’s service-centric rather than network-centric: services register into a service network, consumers discover them by DNS name, and access is governed by IAM policy rather than which subnet you happen to be in. It’s also currently single-region, so it isn’t a drop-in for anything that needs to span regions the way Transit Gateway peering does.

In practice these coexist rather than compete. Transit Gateway keeps doing the job of moving bulk traffic, hybrid connectivity, and anything that isn’t a clean HTTP service call. VPC Lattice picks up new service-to-service communication where IAM-based authorization is a better fit than managing security group rules across account boundaries. I’d reach for Lattice for a new internal API a team wants to expose across accounts without punching new holes in the network layer, not as a project to migrate an existing Transit Gateway backbone onto.

As a decision, it collapses to one real question about the shape of the traffic:

Shaping traffic!

Bulk data, non-HTTP protocols, or anything crossing regions stays on Transit Gateway. A specific, well-defined service boundary within one region is where Lattice earns its keep, and it’s rarely an either-or choice at the level of the whole network.

What’s next

The network is the part of a landing zone that’s genuinely painful to change once workloads depend on it, which is exactly why it deserves the same up-front discipline as the OU structure in Part 1. The last post in this series moves from design to operations: how account vending actually scales once you’re provisioning dozens of accounts a month, why Control Tower can tell you about drift automatically but won’t fix it for you, and the CI/CD pipeline that should sit in front of every change to the guardrails from Part 2 before it reaches a real account.

AWS Landing Zone, Part 2: SCPs, RCPs, and the Logging That Holds Up Under Audit

There’s a specific error message in AWS Control Tower that ruins a Tuesday. Someone edits or detaches a managed SCP (Service Control Policy) on the Security OU (Organization Unit), and the console locks you out with a warning that the shared accounts may no longer be working, and that you shouldn’t provision new accounts until it’s fixed. AWS documents this exact failure mode in its own knowledge base, which tells you it happens often enough to need a canonical writeup. The lesson isn’t “don’t touch SCPs.” It’s that the guardrails Control Tower manages for you and the guardrails you write yourself live in the same policy type, and mixing them up on the wrong OU is how you find out which one was load-bearing.

AWS Landing Zone – Part 2

This is Part 2 of the landing zone series. Part 1 covered what a landing zone is and the build-vs-buy decision behind Control Tower, LZA, and AFT. This one is about the layer that actually does the enforcing: service control policies and their newer sibling, resource control policies, the logging architecture that has to hold up when an auditor asks for it, and IAM Identity Center as the one door every human uses to get into any of this.

SCPs and RCPs aren’t the same guardrail

It’s worth being precise here because the names invite confusion. A service control policy caps what your own identities, users and roles inside your organization, are allowed to do, regardless of what their IAM policy says. A resource control policy caps who can touch a given resource, an S3 bucket, a KMS key, an SQS queue, and a growing list of others, regardless of what that resource’s own policy allows. AWS puts it simply: use an SCP to limit your own principals, use an RCP to restrict access to your resources from principals outside your organization. Neither one grants anything. Both are ceilings, not floors, and the permission a principal ends up with is whatever’s left after intersecting the SCP, the RCP, and the identity or resource policy underneath.

RCPs are the newer of the two, and they’re moving fast. They launched in November 2024 covering five services: S3, STS, KMS, SQS, and Secrets Manager. Since then AWS has kept extending the list. Cognito and CloudWatch Logs picked up support in January 2026, DynamoDB followed a few weeks later, and the per-organization quota doubled to 2,000 RCPs this past July. That pace tells you something: RCPs are still filling in gaps, not yet the mature, complete tool that SCPs are. Check the current supported-service list before designing a control around a service it doesn’t cover yet.

SCPs got their own significant upgrade in September 2025, when AWS gave them full IAM policy language support: conditions inside Allow statements, individual resource ARNs, NotAction with Allow, wildcards in the middle of an Action string. Before that update, SCPs were noticeably blunter than a regular IAM policy, which pushed a lot of teams toward broad deny-everything-except statements because anything more surgical wasn’t expressible. Existing SCPs kept working unchanged after the update, but if yours predate September 2025, there’s a real case for revisiting them now that more precise allow-with-conditions patterns are possible.

Where these guardrails get scoped

Tie this back to the OU structure from Part 1. In practice, almost every SCP and RCP you write gets attached at the Security OU, the Infrastructure OU, the Workloads OU, or somewhere in between, and the Security OU is the one to treat with real caution. That’s where Control Tower’s own managed SCPs live, the ones protecting the Log Archive and Audit accounts, and it’s exactly the OU where the lockout scenario above happens. If you need custom guardrails for security tooling, write them, but write them as additions at a level below where Control Tower’s own policies sit, not as edits to the managed ones.

Put visually, the two guardrail types gate different paths to the same resource:

SCPs,RCPs

The distinction matters operationally, not just semantically. An SCP written to block a risky action only stops your own users and roles from doing it. If the same S3 bucket is reachable by a principal from another AWS account entirely, only an RCP, or the bucket policy underneath it, actually stops that. Teams that treat SCPs as a complete security boundary and skip RCPs are leaving exactly this gap open, usually without realizing it until an access review turns it up.

Centralized logging that holds up under audit

Control Tower sets up an organization-level CloudTrail trail automatically, which, since landing zone version 3.0, replaced the older model of a separate trail per account. One trail logs everything, management account and every member account, and delivers into an S3 bucket that lives in the Log Archive account, the one nobody logs into day to day. AWS Config runs alongside it, aggregating configuration history into the Audit account rather than Log Archive, a distinction worth remembering when someone asks where a specific piece of evidence actually lives.

The pattern that makes this scale past a handful of accounts is delegated administration. Instead of every security service being manageable only from the management account, you designate a member account, almost always the Audit account, as the delegated admin for GuardDuty, Security Hub, Config, and similar services, and manage all of them centrally from there without ever touching the management account for day-to-day work. This has expanded well beyond security services specifically; one recent count put the number of AWS services supporting delegated administration at 37, up from roughly a dozen when the pattern first appeared. GuardDuty is regional, so this has to be repeated per Region you actually monitor, which is easy to miss if you’ve only ever tested in one.

Here’s what that centralization looks like end to end for a single finding:

Sequence of tracking

None of this is exotic engineering. It’s mostly turning on the right delegation and pointing things at the Audit account instead of leaving them scattered. The payoff shows up specifically during an audit, when “show me every API call across every account for the last year” is a single query against one bucket instead of a scavenger hunt across forty.

IAM Identity Center as the only door in

Every human touching any of these accounts should be going through IAM Identity Center, not IAM users with long-lived access keys. The mechanics are straightforward: permission sets define what a person can do, assignments connect a permission set to a group and an account or set of accounts, and Identity Center issues short-lived credentials rather than anything standing. Assign to groups, not individuals. When someone changes teams you move the group membership and every downstream permission follows automatically.

Identity Center federates cleanly with an external identity provider over SAML, Okta, Entra ID, whatever your organization already runs, with SCIM (System for Cross-domain Identity Management) handling user and group provisioning so you’re not managing a second identity store by hand. The one thing every landing zone still needs underneath all of this is break-glass access: a small number of IAM roles or users, outside Identity Center entirely, that work even if federation itself is broken or misconfigured. It’s not a contradiction to have SSO for everything and also keep a locked-down emergency path around it. It’s the same reason a building keeps a physical key next to the electronic badge reader.

One detail worth knowing if you’re serious about treating this whole layer as code: permission sets and their assignments can be managed through a CI/CD pipeline just like the SCPs and RCPs above, JSON templates in a repository, a pipeline reacting to changes and pushing updates to Identity Center. It’s a small thing, but it means access changes go through the same review process as everything else instead of being a console click nobody remembers making six months later.

What’s next

Guardrails and identity get most of the attention in landing zone design because they’re where the compliance conversations happen, but the networking layer underneath all of this has its own decisions and its own ways to quietly overspend. Part 3 covers Transit Gateway and the hub-and-spoke pattern almost everyone converges on, centralized egress and what it actually costs against a pile of per-VPC NAT gateways, IPAM as the CIDR governance layer you want in place before account number one, and where VPC Lattice fits next to Transit Gateway instead of replacing it.

Amazon CloudWatch Managed Prometheus Collectors: Retiring the Self-Managed OTel Collector

Ask anyone who has wired Prometheus metrics into CloudWatch what the real bottleneck was, and it was never Prometheus. It was the collector. You’d stand up an OpenTelemetry Collector as a Deployment or a DaemonSet, hand-tune its scrape config, guess at memory limits for cardinality you hadn’t measured yet, and then spend the next year treating it like one more piece of cluster infrastructure that needed patching, right-sizing, and on-call attention of its own.

AWS Managed Prometheus Collectors

On July 31, AWS announced managed Prometheus collectors for Amazon CloudWatch, a fully managed, agentless scraper for Amazon EKS, Amazon EC2, Amazon ECS, Amazon MSK, and Amazon OpenSearch Service. You supply a scrape configuration and a way to reach your resources, and CloudWatch takes it from there: provisioning the collector, scaling it, and keeping it running. What it scrapes arrives in OpenTelemetry format, queryable with PromQL right next to your AWS vended metrics.

That’s the announcement. The more useful question, if you’ve spent any time in AWS’s observability stack already, is what’s actually new here versus what’s just wearing a new badge.

The new part is the destination, not the collector

This isn’t new collector technology. AWS shipped this same agentless scraper (automatic target discovery, no in-cluster agent, multi-AZ, metrics that never leave your VPC) as the Amazon Managed Service for Prometheus collector back in November 2023, initially scoped to EKS and writing into an AMP workspace. That lineage is visible directly in the API today: you still create one of these with the CreateScraper operation, under the aws amp CLI namespace, not some new CloudWatch-specific command. What’s new in this announcement is the destination and the source list. Alongside an AMP workspace, the same managed collector can now write into a CloudWatch dataset, and the supported sources have grown from EKS-only to include EC2, ECS, MSK, and OpenSearch.

Here’s an EKS scraper creation, adapted from AWS’s setup docs:

aws amp create-scraper \
  --alias "eks-metrics-scraper" \
  --source eksConfiguration="{clusterArn='arn:aws:eks:us-west-2:111122223333:cluster/prod-cluster', \
    securityGroupIds=['sg-0123456789abcdef0'], \
    subnetIds=['subnet-0aaa111','subnet-0bbb222']}" \
  --scrape-configuration configurationBlob=$(cat eks-scrape-config.yaml | base64 -w 0) \
  --destination cloudWatchConfiguration="{datasetArn='arn:aws:cloudwatch:us-west-2:111122223333:dataset/default'}"

Swap that final --destination flag for an ampConfiguration with a workspace ARN and you’re back to the exact collector AWS shipped in 2023. If you’re already running AMP collectors for Grafana dashboards, that’s the useful takeaway: this is an incremental destination option on infrastructure you may already trust, not a new product to evaluate from a cold start.

It also builds on something CloudWatch shipped only six weeks earlier: native OTLP ingestion with PromQL querying, launched June 9. That release is what made CloudWatch a legitimate destination for Prometheus-shaped metrics at all, with per-GB pricing and curated Container Insights dashboards for EKS. Managed Prometheus collectors are the agentless front door to that same pipeline, extended past what Container Insights auto-instruments.

How a scrape actually reaches CloudWatch

The mechanics will feel familiar if you’ve used any managed AWS scraper before. You hand the collector a set of subnets, and it creates an Elastic Network Interface in each one. It scrapes your targets over those ENIs using OTLP, then delivers the result to your CloudWatch dataset through a VPC endpoint. Nothing crosses the public internet.

What isn’t obvious from the diagram is that target discovery differs meaningfully by source, and that’s what determines how much of an existing scrape config actually survives the move:

  • EKS uses Kubernetes service discovery against the cluster API, plus control-plane metrics (kube-scheduler and kube-controller-manager, exposed starting at Kubernetes 1.28).
  • ECS uses DNS-based discovery through AWS Cloud Map, so tasks register once and get picked up without a static IP list.
  • EC2 is static_configs against private IPs, no different from pointing a self-managed setup at a fixed target list.
  • MSK uses DNS-based discovery against the cluster’s own bootstrap DNS name, which resolves to every broker and survives broker replacement without reconfiguration.

MSK is worth a closer look because it needs a prerequisite step: enabling Open Monitoring on the cluster, which exposes a JMX Exporter on port 11001 and a Node Exporter on port 11002. A scrape config covering both looks like this:

global:
  scrape_interval: 60s
  external_labels:
    cluster_name: my-msk-cluster

scrape_configs:
  - job_name: 'msk-jmx'
    dns_sd_configs:
      - names: ['my-cluster.abc123.c4.kafka.us-west-2.amazonaws.com']
        type: A
        port: 11001
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance

  - job_name: 'msk-node'
    dns_sd_configs:
      - names: ['my-cluster.abc123.c4.kafka.us-west-2.amazonaws.com']
        type: A
        port: 11002
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance

That gets you broker-level Kafka metrics (topic throughput, consumer lag, under-replicated partitions) from the JMX exporter, and host-level CPU, memory, and disk metrics from Node Exporter, both queryable with PromQL once they land. Two things to check before you plan around this: MSK Serverless and MSK Express aren’t supported, and public access combined with KRaft metadata mode is excluded too.

The scrape config is Prometheus-compatible, not Prometheus

This is worth reading closely before you migrate anything, because “Prometheus-compatible YAML” undersells how much is actually missing. The supported configuration surface covers global settings, scrape_configs with static_configs and dns_sd_configs, and relabeling. What isn’t there matters more than what is:

  • Minimum scrape interval is 30 seconds. An SLO built on 10 or 15-second scrapes doesn’t fit here.
  • No file_sd_configs, and no Consul, Eureka, or other external service discovery. If your target list comes from anywhere other than Kubernetes, Cloud Map, a static list, or MSK’s broker DNS, this collector can’t reach it.
  • remote_write and remote_read don’t apply, because delivering to CloudWatch is the whole job. A fan-out to a second backend doesn’t carry over.
  • Configuration blobs cap out at 256 KB, base64-encoded.

None of this is really a flaw. It’s the standard trade-off of a managed service. AWS narrowed the surface to what it can operate reliably at scale, and the narrowing happens to remove exactly the parts of Prometheus configuration that are hardest to run safely inside someone else’s control plane. But “copy your existing prometheus.yml over” is rarely literally true. Diff your current scrape configs against this list before you commit to a cutover date, not after.

What ships automatically, and what doesn’t

EKS and MSK both get a curated dashboard the moment metrics start flowing — EKS OTel and MSK OTel in the CloudWatch console — plus attribute enrichment on every metric: account, region, an inferred unit, and for EKS specifically the cluster name and ARN. That enrichment is what makes PromQL filtering pleasant instead of an exercise in label archaeology. The EC2/ECS path doesn’t come with an automatic dashboard, which is a reasonable trade given how varied EC2/ECS metric shapes are in practice, but it’s worth knowing before cutover rather than after.

Operationally, a managed collector vends its own logs to CloudWatch Logs covering target discovery, scrape successes and failures, and configuration errors, and you manage the scraper itself with aws amp list-scrapers, describe-scraper, and delete-scraper. Deleting one tears down its ENIs, and that cleanup takes a few minutes, so don’t script an immediate re-create into the same subnet.

Cross-account guidance has shifted too. Instead of the role-chaining pattern AMP has historically used for cross-account scrapers, AWS’s current recommendation for both EKS and MSK is CloudWatch metric centralization: replicate the metrics to a monitoring account rather than granting the scraper cross-account reach in the first place.

What this actually costs

Pricing has two components that scale independently, and it’s worth keeping them separate. The collector itself is billed hourly, similar in shape to an ENI or a NAT gateway, though the specific per-hour rate isn’t broken out yet on AWS’s public pricing page as of this writing. What is confirmed is that everything the collector delivers rides on standard CloudWatch OpenTelemetry ingestion pricing: currently $0.50 per GB for general OTel metric publishing, a flat rate that bundles in fifteen months of storage with no separate per-metric or per-API charge. A typical data point with ten to fifteen attributes runs 300 to 600 bytes, for a sense of scale. Console PromQL queries (Query Studio, dashboards) are free; programmatic PromQL queries cost $0.01 per million samples scanned.

That per-GB model is a real shift in how you’d think about cost here. Classic CloudWatch metrics billed per unique metric-dimension combination, so cardinality was the thing to fear. Under OTel ingestion, cardinality by itself is free; what costs money is the serialized size of each data point, which grows with the number and length of the labels attached to it. Fifteen short labels on a metric barely move the needle regardless of series count. A handful of long, high-cardinality values move it fast: unbounded request IDs, verbose ARNs repeated on every point. If you’re porting scrape configs over from a self-managed setup, that’s the moment to prune labels you’ve been carrying out of habit rather than actual query need.

One more line item: scraping itself can rack up VPC data transfer charges between the collector and your targets, separate from CloudWatch ingestion. AWS’s own suggestion is to enable gzip on your /metrics endpoints, which cuts transfer cost without changing what actually gets ingested.

Where I’d actually reach for this

If your team is already CloudWatch-centric and has been putting off a real Prometheus setup mainly because nobody wanted to own collector infrastructure, this closes that gap cleanly. For EKS and MSK metrics specifically, it’s a solid default: alarms and dashboards in the same place as everything else, without standing up Grafana and an AMP workspace just to get there.

I’d still keep a self-managed collector, or the AMP-workspace version of this same scraper, in a handful of specific situations: sub-30-second scrape requirements, target discovery through Consul, Eureka, or file-based service discovery, or a remote_write fan-out to more than one backend because you’re mid-migration or running a deliberately multi-vendor setup on purpose. None of those are rare for a mature platform team, so this doesn’t replace the self-managed path outright. It’s a genuinely good default for the common case, which happens to be most of it.

Availability is broad but not universal — it’s live everywhere CloudWatch’s OTLP endpoint already exists, minus Asia Pacific (New Zealand) for now. Worth checking before you write the migration runbook, not after you’ve scheduled it.

SKILL.md: Stop Re-Explaining Your Workflow to Claude Every Session

Somewhere in your notes app, or maybe just muscle memory, there’s a paragraph you retype into Claude at the start of almost every work session. The AWS tags that are actually mandatory versus the ones nobody enforces. The five things a pull request review always has to check before it counts as “done.” Which environments tolerate an open security group and which ones get you a message from the security team five minutes later. You paste it in, the model gets up to speed, and you move on to the actual task. Next week you paste it in again, because the model has no memory of last Tuesday, and the teammate who just joined your channel has never seen that paragraph at all.

Skill.md!

That’s the exact problem Anthropic built Agent Skills to solve, and the fix is almost embarrassingly low-tech: a folder with a markdown file in it, named SKILL.md.

What a SKILL.md file actually is

A Skill is a directory. At minimum it holds one file, SKILL.md, which opens with a short YAML header (called frontmatter) and is followed by ordinary markdown instructions. That’s the entire format. No special runtime, no proprietary config language, nothing to compile.

If you’ve ever asked Claude to build you a PowerPoint deck or clean up an Excel sheet, you’ve already used one without realizing it. The document tools Claude ships with for PowerPoint, Excel, Word, and PDF are, under the hood, ordinary Skills, the same folder-and-markdown format you’d use to build your own.

Anthropic frames building a Skill as putting together an onboarding packet for a new hire: here’s what you need to know, here’s how we do things, here’s where to look if you get stuck further in. The engineering post announcing the feature, published on October 16, 2025, walks through this using a PDF-handling Skill as its running example, and the team signs off the post with a joke about their shared fondness for folders. Fair enough — a Skill really is just a folder that happens to be very good at its one job.

Beyond instructions, a Skill can bundle reference docs, templates, and actual executable scripts, all living in the same directory. Claude reads whatever it needs and leaves the rest sitting on disk untouched. That last part turns out to be the whole point of the design.

The problem this actually solves

Before Skills, there were two bad options for capturing “how we do things here.” Cram everything into one enormous system prompt, and Claude carries your entire team’s handbook in its head on every single request, most of it irrelevant to whatever you actually asked. Or build a separate, narrowly scoped agent for every recurring task, which stops scaling somewhere around the third or fourth workflow.

Skills get around both by loading information in stages, a pattern Anthropic calls progressive disclosure. None of a Skill’s full content sits in context until Claude decides it’s actually relevant to the request in front of it.

How the loading actually works

There are three levels, and the token cost changes sharply between them, according to Anthropic’s documentation:

LevelWhen it loadsRough costWhat’s in it
1. MetadataAlways, at startup~100 tokens per SkillJust the name and description
2. InstructionsOnly when the Skill is triggeredAim under ~5k tokens (body under 500 lines)The markdown body: workflow, rules, templates
3. Resources and scriptsOnly as neededNothing, until read or runReference files, templates, executable scripts

Install thirty Skills and Claude carries thirty short descriptions around at all times, which is cheap. It reads the full body of exactly one of them: whichever matches your request. Anything that Skill bundles beyond its own SKILL.md, a reference doc or a Python script, stays untouched on the filesystem unless Claude specifically goes looking.

Here’s what that looks like end to end, using a Skill built to review Terraform plans before they’re applied:

Skills flow example

Notice the script step. Claude runs check_tags.py through bash and only the output, a pass or a list of violations, comes back into context. The script’s actual code never loads. It’s also why Anthropic’s guidance leans toward writing real utility scripts for anything deterministic, tag validation, checking for overlapping form fields, whatever the equivalent is in your domain, rather than asking Claude to regenerate that logic from scratch every single time. A script is more reliable, and it’s nearly free, token-wise.

When, and why, to reach for one

Skills earn their keep on anything you’d otherwise explain more than once. A few honest signals it’s worth the twenty minutes:

  • You’ve caught yourself pasting the same context, checklist, or gotcha into more than two or three conversations already
  • The task follows a procedure your team already agreed on, a review process, a report format, a migration sequence, and you want everyone’s output to actually look the same
  • You want to stack several of these specialized capabilities in one session; Skills compose, so a single request can trigger more than one at a time

Where beginners usually get tangled is the line between a Skill and MCP (Model Context Protocol). A Skill is packaged know-how: instructions, and optionally code, sitting on Claude’s filesystem. It is not a live connection to Jira, a production database, or your calendar. That’s exactly what MCP connectors exist for. The two aren’t rivals. A Skill’s instructions can absolutely tell Claude to call a specific MCP tool partway through a workflow, and Anthropic’s own guidance is to reference it by its fully qualified name, something like GitHub:create_issue, so Claude doesn’t confuse it with a similarly named tool from a different server. The Skill supplies the how and the when. MCP supplies the reach into a live external system.

Skills and MCP

Anatomy of a real one

Here’s a small but complete Skill: the same Terraform-review example from the diagram above.

reviewing-terraform-plans/
├── SKILL.md
├── RULES.md
└── scripts/
    └── check_tags.py

---
name: reviewing-terraform-plans
description: Reviews Terraform plan output against infrastructure safety rules before apply. Flags unapproved destroys, missing required tags, and security groups or storage buckets opened to the public internet. Use whenever the user pastes terraform plan output, uploads a plan file, or asks whether a Terraform change is safe to apply.
---

# Reviewing Terraform Plans

## Quick workflow

1. Read the pasted plan output, or run `terraform show -json tfplan.binary` if a binary plan file was provided.
2. Flag every resource marked for **destroy** or **replace**. Check [RULES.md](RULES.md) for whether that resource type needs a second approver before it's safe to apply.
3. Verify tagging by running `python scripts/check_tags.py plan.json` instead of reading the JSON by eye. It lists every resource missing an `owner` or `environment` tag.
4. Flag any security group, storage bucket policy, or load balancer rule that widens access to `0.0.0.0/0`.
5. Write up the findings using the format below.

## Report format

Always structure the review like this:

```
## Plan Review: [environment]
### Blocking
### Needs a second look
### Looks safe
```

Only use "Looks safe" for resources that passed both the destroy check and the tag check.

## Rules for destroys and replaces

See [RULES.md](RULES.md) for the sign-off matrix: which resource types need a second approver, which are safe to auto-approve, and how to write a rollback note.

## Utility scripts

**scripts/check_tags.py** — validates required tags against a JSON plan export. Exits non-zero and lists offending resources if anything is missing `owner` or `environment`.

Now the walk-through. The frontmatter is the only mandatory part, and both of its fields do real work rather than just labeling the file.

name has firm rules:

  • Maximum 64 characters
  • Lowercase letters, numbers, and hyphens only
  • No XML tags
  • Can’t contain “anthropic” or “claude”

Anthropic’s naming guidance recommends a gerund form, like reviewing-terraform-plans or processing-pdfs, on the theory that it reads as an activity rather than a vague noun like helper or utils. That’s a recommendation, not a hard rule, but it’s a good one.

description matters more than it looks like it should, because it’s the only thing Claude sees before deciding whether to open the file at all:

  • Non-empty, maximum 1,024 characters
  • No XML tags
  • Written in third person, not “I can help you review…”
  • Must cover both what the Skill does and when to reach for it

Get this vague, “helps with infrastructure,” say, and the Skill just won’t fire reliably, because Claude is pattern-matching your actual request against this exact text before it ever reads the body.

Everything below the frontmatter is “Level 2,” read only once the description matches. The workflow section is a numbered procedure rather than loose prose, because this is what Anthropic calls a narrow-bridge task: there’s basically one correct order of operations, so it gets specific, low-freedom steps instead of general guidance. A code-review Skill, by contrast, is an open-field task. Several valid approaches exist depending on context, so the right move there is a looser set of principles Claude can apply with judgment.

The report format is a plain template. Claude copies the structure rather than inventing its own headers each time, which is what keeps five plan reviews from five different engineers from looking like five different documents.

RULES.md is the interesting one. It’s referenced by name but not pulled into SKILL.md itself, so it costs zero tokens unless a destroy actually shows up in the plan. That’s progressive disclosure paying off directly: most Terraform plans don’t touch anything destructive, so most reviews never load it at all.

And check_tags.py exists because validating tag presence is exactly the kind of deterministic, no-judgment-required task a script handles more reliably, and far more cheaply, than having Claude parse JSON by eye on every single run.

Where it’s not worth the effort

A few situations where reaching for a Skill is the wrong move.

One-off tasks. If you’re never going to ask for this again, just ask directly. Claude tends to only bother consulting a Skill for something it can’t already handle cleanly with its built-in tools, so a simple “summarize this PDF” often won’t trigger a Skill at all, matching description or not, because there was no real gap for the Skill to fill.

A single fact or preference. “I prefer metric units” isn’t a workflow. It’s a preference, and it belongs in your settings or a saved memory, not a folder with a YAML header.

Anything that needs a live connection to an external system. As covered above, that’s MCP’s job.

Content with a shelf life. Baking in “as of this month, use the v1 API” is a trap. Anthropic’s own best-practices guidance calls this out directly: keep only the current method in the main instructions, and tuck anything deprecated into a collapsed “old patterns” section instead of sprinkling date-conditional logic through the file. A Skill that says “if it’s before August, do X” is simply wrong the moment August ends.

It’s a standard now, not just a Claude feature

Skills launched on October 16, 2025 as an Anthropic-specific mechanism across Claude.ai, Claude Code, the Claude Developer Platform, and the Agent SDK. On December 18, 2025, Anthropic published the format as an open, cross-platform standard, meaning a well-written SKILL.md folder isn’t locked to one vendor. At the time of writing, the same format is supported, with varying degrees of completeness, by dozens of other agent tools, including Cursor, GitHub Copilot, VS Code, Gemini CLI, and OpenAI Codex.

Practically, that’s a reason to actually invest in writing these well. A Skill your team builds for Claude Code today is a reasonable bet to keep working if part of your toolchain moves somewhere else next year.

Guidelines worth following even though nothing enforces them

Beyond the hard constraints on name and description, Anthropic’s best-practices guide is mostly soft guidance. It’s worth taking seriously anyway.

Keep it concise. The default assumption should be that Claude already knows what a PDF is or how a REST API works, so don’t spend tokens re-explaining things it already knows. Match your level of specificity to how fragile the task actually is: loose, principle-based instructions for judgment calls, and exact scripts with no room for improvisation anywhere one wrong step corrupts data. Anthropic’s own comparison is a narrow bridge versus an open field, and it’s a genuinely useful way to decide how much rope to give.

Keep reference files exactly one level deep from SKILL.md. If Claude has to follow a chain of three linked files to find the actual instruction, it tends to skim with something like head -100 rather than read the whole thing, and you lose information you thought you’d included. And build a few test cases before writing extensive documentation, not after, so you’re solving problems Claude actually has rather than ones you imagined it might have.

Guardrails: treat a Skill like software, because it is one

This part is easy to skip past, and shouldn’t be. A Skill can execute code and invoke tools, which means a careless or malicious one can do real damage: read files it shouldn’t, reach out to an external URL with data picked up along the way, or quietly do something other than what its own description claims.

Anthropic’s security guidance is blunt about it: only install Skills from sources you trust, yourself or Anthropic directly, and if you’re using one from anywhere else, audit every file in the directory first, not just SKILL.md. Pay particular attention to anything that reaches out to an external URL, since fetched content can carry instructions of its own that Claude never asked for.

If you’re rolling Skills out across a team, Anthropic’s enterprise guidance is direct about treating this like any other software approval process rather than something looser: run a risk assessment before deployment, require an evaluation suite of representative queries covering cases where the Skill should trigger, shouldn’t, and sits ambiguously in between, and don’t let a Skill anywhere near production until it clears that bar. The general shape most teams land on beyond that: someone other than the author reviews the actual instructions and code, it gets tried out in an isolated environment first, and there’s a simple record somewhere of what’s approved and who owns it. “It’s just a markdown file” is exactly the assumption that causes problems once that markdown file starts running Python.

It’s also worth knowing the sandbox itself isn’t identical everywhere. Skills running through the Claude API get no network access and can’t install packages at runtime; whatever’s pre-installed is what you get. Skills in Claude Code have the same network access as anything else running on your machine. That difference alone should shape what you’re willing to let a given Skill actually do.

Actually building one

The fastest way in is almost too simple to feel like real advice: ask Claude to write it. Claude already understands the SKILL.md format natively, so work through a task normally, then say something like “turn this into a Skill,” and it’ll produce a properly structured file with sensible frontmatter on the first pass. From there, the loop that actually improves a Skill is boring but effective: use it on a real task, notice where Claude reached for the wrong file or skipped a rule, and go fix that specific gap instead of rewriting the whole thing speculatively.

For a more structured on-ramp, Anthropic’s quickstart and cookbook cover the API side end to end, and DeepLearning.AI has a short course built with Anthropic if you’d rather work through examples with someone walking alongside you.

Either way, the actual test for whether a Skill is worth keeping is the same test you’d apply to any piece of internal documentation: does the next person who hits this task get through it faster because the file exists? If yes, it’s earning its space on disk. If not, it’s just another file nobody reads, which, folder or not, is the exact problem you were trying to get away from in the first place.

AI has two price tags in IT Work — One in Dollars, One in Skill

You merge a pull request that Cursor wrote for you in ninety seconds. By hand, it would have taken the better part of an hour. For a moment you feel like you’ve found a genuine shortcut. In July 2025, the AI research nonprofit METR ran the most careful study anyone has done on that exact feeling, and found that experienced developers using AI tools on codebases they already knew well took 19% longer than developers doing the same work by hand. Afterward, the AI group still believed they’d been sped up by 20%. Same people, same tasks, two completely different numbers.

Price of an AI

That gap, between what AI feels like it’s saving you and what it’s actually costing you, is what this post is about. Almost all of the rigorous measurement on this so far is about software developers specifically, since developers leave a paper trail of commits and tickets that’s unusually easy to study. Swap “pull request” for “change ticket” or “runbook,” though, and the same shape shows up for sysadmins, SREs, and support desks too. The costs land in two currencies: the dollars on next quarter’s invoice, and the skill you quietly stop building because the model always answers first.

The productivity story is messier than either side wants it to be

METR’s result held up because the study design was unusually tight. Sixteen experienced developers, working in mature open-source projects they’d averaged five years on, were randomly told to use or not use AI tools on 246 real issues they’d have tackled anyway. This wasn’t a benchmark or a survey. It was a stopwatch on real work, in codebases these developers already understood cold, and that detail matters more than the headline number. The slowdown showed up specifically where the developer already had deep context.

METR tried to re-run the study with newer tools starting in August 2025, and by February 2026 they’d run into a problem worth mentioning on its own terms: a large share of the developers they invited, by some accounts as much as half, declined to take part at all unless they were guaranteed AI access. That’s not really a productivity statistic. That’s a dependency statistic. Among the smaller cohort who did participate, the slowdown shrank to something close to statistical noise, and METR now says, based on conversations with participants, that it believes AI is probably providing a genuine benefit in early 2026, while being upfront that its data is too thin to say how much.

None of this means AI coding tools are a bust. Google’s 2025 DORA report, drawing on nearly 5,000 technology professionals, calls AI “the great amplifier”: it doesn’t fix a struggling team or break a strong one, it magnifies whatever was already there. Unlike the year before, the 2025 data showed a positive relationship between AI adoption and both delivery throughput and product performance. Adoption still correlated with worse delivery stability, though, and DORA’s explanation is that acceleration without strong automated testing, clean version control, and fast feedback loops just means more change volume hitting a system that can’t absorb it safely.

Separately, Faros AI pulled telemetry from over 10,000 developers and told a version of the same story from another angle. On teams with heavy AI adoption, individual developers handled 9% more tasks and shipped 47% more pull requests per day, but none of that translated into their organizations delivering software noticeably faster overall. Doing more and delivering more turned out to be different things.

My read, watching this play out on real teams, is that the studies agree more than they disagree. AI’s payoff depends on how unfamiliar the terrain is and how strong the guardrails are around it. It’s least useful in exactly the place an experienced IT professional already has an advantage: their own well-understood system.

The dollar price tag

Uber’s leadership was unusually candid about what happened next. By April 2026, four months into the year, the company had already burned through its entire annual budget for AI coding tools. CEO Dara Khosrowshahi put it plainly on a podcast that June: “we blew through our AI budget in a quarter,” for what was meant to last the whole year. Uber’s fix was the one most FinOps teams eventually reach for anyway: a hard per-employee cap of $1,500 a month per agentic coding tool, visible on an internal dashboard so people can watch themselves approach the limit. Walmart quietly capped its own internal agent around the same time. Microsoft, according to reporting picked up by Fortune, began pulling back most of its direct Claude Code seats in favor of GitHub’s own Copilot CLI, for what looks like the identical underlying reason: nobody had modeled what happens once an agent, rather than a person, is the one deciding how many tokens to spend.

This isn’t really a story about one vendor mispricing its product. It’s a story about an entire category changing its unit economics mid-flight. GitHub moved Copilot off flat per-seat pricing and onto token-metered “AI Credits” in June 2026, because a one-line chat question and an hour-long autonomous coding session had stopped being remotely comparable in what they actually cost to serve. Cursor made a similar move the year before. Somewhere on Reddit, one developer described their monthly bill jumping by something like ten to twenty-seven times once metered billing kicked in, not because their habits changed, but because the same habits suddenly cost differently.

If you’re the one setting policy for a team, here’s the uncomfortable part: flat-fee subscriptions were hiding the real cost curve, which made it easy to justify sprinkling AI into everything on the assumption it was basically free at the margin. A February 2026 survey of 500 finance leaders found that 79% of enterprises had experienced an AI cost overrun in the prior twelve months, and the counterintuitive detail in that data is that overruns got worse, not better, as organizations’ cost-tracking practices matured. Mature FinOps teams aren’t actually worse at control. They’re just better at measuring, which means they’re the first ones to see the bill they’d already been running up in the dark. If nobody on your team can say what a specific agent run cost by tomorrow morning, that’s the gap worth closing before you expand usage, not after.

The skill price tag

The more interesting cost never shows up on an invoice at all. In June 2025, researchers at MIT Media Lab wired 54 people with EEG while they wrote essays over several months, some using ChatGPT, some using a search engine, some with no tools at all. The language here is deliberately careful, because the researchers themselves asked journalists not to describe the results as “brain rot” or “damage,” and the paper itself doesn’t use those words either. What they found instead was that the group using the LLM showed the weakest neural connectivity of the three, reported the lowest sense of ownership over what they’d written, and struggled afterward to accurately quote their own essays back to themselves. A smaller crossover group, who’d already spent months writing unaided before switching to AI help, didn’t show the same drop-off. They had something to fall back on.

That detail, that prior practice seems to protect you, turns up again in research Anthropic itself published this January. Researchers had developers learn a new asynchronous Python library called Trio, some with AI assistance and some without, then tested what they actually understood afterward. AI use measurably hurt conceptual understanding, code reading, and debugging ability, without buying most participants a real speed advantage. Only the people who fully delegated the work to the AI finished faster, and they paid for it in comprehension. It’s an unusual thing for a company that sells an AI coding agent to publish, which is exactly why it’s worth taking seriously.

There’s a pattern across both papers worth sitting with. The cost isn’t AI use in general. It’s AI use during the window before you’ve built a mental model of whatever you’re working on. A senior engineer running an agent against a service she’s operated for six years is drawing on schema she already has, and the tool can’t erode understanding that’s already load-bearing. A new hire pointed at that same service for the first time, with no schema yet, is in a completely different position, and whatever they don’t build now, they won’t have later. The tokens don’t refund the difference. For what it’s worth, the most useful finding in the Anthropic paper wasn’t the headline number. Researchers identified six distinct patterns in how people actually used the AI, and in the three where participants stayed cognitively engaged, asking the model to explain itself rather than just produce output, the learning outcomes held up even with AI in the loop.

METR’s dependency finding from the last section belongs in this pile too. Refusing to even attempt paid work without AI access isn’t a preference. It’s what skill erosion looks like from the inside, well before anyone gets around to measuring it.

The bill that shows up in the incident report instead

Security is where the dollar cost and the skill cost turn out to be the same cost wearing two hats. Veracode’s testing of over 100 language models across dozens of coding tasks in Java, Python, C#, and JavaScript found that 45% of the AI-generated samples introduced an OWASP Top 10 vulnerability, a pass rate that hasn’t meaningfully improved across testing cycles despite vendor claims to the contrary. Java came out worst, failing secure generation more than seventy percent of the time. Separate analysis from CodeRabbit put AI-authored pull requests at roughly 2.7 times the vulnerability density of human-written ones, and GitGuardian found 6.4% of repositories using GitHub Copilot leaking at least one secret, against a 4.6% baseline in repositories without it.

None of that should be surprising on its own. Models optimize for code that runs, not code that survives an attacker who’s read the same training data. What should worry an IT professional more is a pair of academic findings on Copilot that have held up since some of the earliest research on the tool. One of the first studies, by Pearce and colleagues, found that roughly 40% of Copilot’s suggested programs contained a vulnerability. A follow-up by Perry and colleagues went further and found that developers given AI assistance wrote measurably less secure code than developers working without it, and rated their own insecure solutions as secure more often. That’s the same false-confidence pattern MIT found in a totally different context, just wearing a security badge instead of an essay grade. The tool doesn’t only introduce the vulnerability. It quietly turns down your own alarm for noticing one.

That’s the plainest case for keeping a human genuinely in the loop rather than nominally in the loop. Not because AI-written code is always wrong, but because it’s wrong in ways that look right, to reviewers who’ve been trained by months of mostly-correct output to stop looking as hard.

So when do I actually reach for it

After all of that, here’s roughly where I’ve landed for my own team:

Reach for AIThink twice
Boilerplate, scaffolding, first-draft testsCode in a system you already know cold
A stack you’re deliberately exploringA junior’s first unsupervised weeks on an unfamiliar service
Sandboxes, non-prod IaC, throwaway scriptsAnything touching auth, payments, or PII
Log summaries, docs, low-stakes ticket triageSecurity-sensitive code with no review time budgeted
Translating between two things you already understandA judgment call you’ll want to have made yourself, later

The shape underneath that table is closer to a flowchart than a fixed rule, so here’s the actual decision I run through before reaching for a coding agent:

Two questions do most of the work here: do you already have a mental model of what you’re touching, and what happens if the output is wrong and nobody catches it for a week. Everything else, like which model or which vendor, is implementation detail that will have changed again by the time you read this.

Treat it like any other total-cost-of-ownership call

Every cost in this post is a total-cost-of-ownership problem, and IT professionals already know how to run those. Nobody greenlights a new platform because the sticker price looked reasonable without asking what it costs to operate and support over the next three years. AI tooling deserves that same discipline, just split across two ledgers instead of one: what it costs in tokens, and what it costs in the understanding your team would have built anyway by doing the work themselves.

That pull request Cursor wrote in ninety seconds probably did save you time. Just budget for the chance that it didn’t, that it needs a harder look than you’re inclined to give it, and that the muscle you skipped today is the one you’ll need on the day the model is wrong and you’re the only person who’d have known.

What I’d actually do on Monday

Instrument both meters, because right now most organisations are flying blind on one and in denial about the other.

On spend: get per-team token attribution before you get a budget surprise, set caps at the gateway rather than in a policy document nobody reads, and stop assuming that a more capable model is a more expensive model’s replacement rather than its addition.

On cognition: pick one thing per quarter that you learn the slow way. Not everything — that’s martyrdom, not strategy. One thing you’ll need to own, that you deliberately struggle through without the assistant. Getting painfully stuck is not wasted time; it is how the debugging skill gets built, and the debugging skill is what makes you worth having in the loop at all.

The engineers who’ll be valuable in three years aren’t the ones who avoided these tools. They’re the ones who stayed able to tell when the tools were wrong.

AI Agents, Explained: A Guide for Beginners

If you’ve spent any time around technology in the last couple of years, you’ve heard the word “agent” thrown around a lot — usually with more excitement than precision. This post is an attempt to fix that. I’ll explain what an AI agent actually is, when you should (and shouldn’t) reach for one, how they work under the hood, the vocabulary you’ll keep bumping into, and the two things that decide whether an agent project succeeds in production: cost and safety.

AI agents!

I’ve written it to be readable if you’re brand new to AI, while still being concrete enough for an IT professional who has to make systems, design or architecture decisions.

First, what is an “agent”?

The idea is older than the current AI wave. In classic computer science, an intelligent agent is defined as “an entity that perceives its environment, takes actions autonomously to achieve goals, and may improve its performance through machine learning or by acquiring knowledge” (Wikipedia). That textbook framing — perceive, decide, act — still holds today.

The modern, cloud-vendor version says much the same thing in plainer words. AWS defines an AI agent as “a software program that can interact with its environment, collect data, and use that data to perform self-directed tasks that meet predetermined goals” (AWS). The key word is self-directed: you give it a goal, not a step-by-step script.

What’s new is the brain. In today’s agents, the control flow is “frequently driven by large language models” (Wikipedia). The LLM is what lets the agent understand a fuzzy instruction in plain English, decide what to do next, and adapt when things don’t go as planned.

The one distinction that clears up most confusion: workflows vs. agents

Here’s the single most useful mental model I’ve found, from Anthropic’s engineering team. They separate “agentic systems” into two categories:

  • Workflows are “systems where LLMs and tools are orchestrated through predefined code paths.”
  • Agents are “systems where LLMs dynamically direct their own processes and tool usage, maintaining control over how they accomplish tasks” (Anthropic).

In a workflow, you wrote the flowchart and the LLM fills in the boxes. In an agent, the model decides the flowchart at runtime. That difference drives everything else — predictability, cost, and risk all shift when you hand the steering wheel to the model.

How an agent actually works

Strip away the hype and most LLM agents are, in Anthropic’s words, “just LLMs using tools based on environmental feedback in a loop” (Anthropic). That loop is the whole thing:

Walking through it the way AWS frames the process — determine the goal, acquire information, implement tasks, then evaluate outcomes and adjust (AWS):

  1. Goal in. A person gives the agent a task in natural language. A good agent will pause to clarify an ambiguous request before charging ahead.
  2. Reason and plan. The LLM decides what the next step should be.
  3. Act via a tool. The agent does something in the real world — queries a database, calls an API, runs code, sends an email.
  4. Observe. Critically, the agent must “gain ‘ground truth’ from the environment at each step (such as tool call results or code execution) to assess its progress” (Anthropic). This feedback is what separates an agent from a chatbot that just guesses.
  5. Loop or stop. It repeats until the goal is met or a stopping condition trips.

The building block: an “augmented LLM”

The atom of every agent is what Anthropic calls the augmented LLM — a language model enhanced with three capabilities: retrieval, tools, and memory (Anthropic). Modern models can actively generate their own search queries, pick the right tool, and decide what information is worth keeping. AWS describes a similar architecture: a foundation model, a planning module, a memory module, tool integration, and a learning/reflection mechanism (AWS).

The vocabulary you’ll keep hearing

Tools. A tool is anything the agent can call to affect or observe the world — a search API, a calculator, a code interpreter, a database query. The quality of your tool descriptions matters more than people expect. Anthropic recommends “poka-yoke” design (a manufacturing term for mistake-proofing) — writing tool parameters and descriptions so clearly, with examples and edge cases, that it’s hard for the model to misuse them (Anthropic).

Skills. A newer concept worth knowing: Skills are “folders that include instructions, scripts, and resources that Claude can load when needed” (Anthropic). They work through progressive disclosure — the agent scans available skills, and “when one matches, it loads only the minimal information and files needed,” which keeps it fast while still having specialized expertise on tap. Skills are composable (they stack), portable (“build once, use across Claude apps, Claude Code, and API”), and efficient because they only load what’s needed, when it’s needed (Anthropic). Think of a skill as a reusable competency you can hand to an agent, rather than cramming everything into one giant prompt.

MCP (Model Context Protocol). As soon as you want an agent to talk to real systems, you hit an integration problem: every tool has its own API. MCP is “an open-source standard for connecting AI applications to external systems.” The docs use a nice analogy — “think of MCP like a USB-C port for AI applications,” a standardized way to plug an agent into data sources, tools, and workflows (MCP). For IT teams, the appeal is “build once and integrate everywhere” instead of writing a bespoke connector per tool.

Memory. Agents distinguish between short-term memory (the current task’s context) and longer-term memory (things worth keeping across sessions). It’s one of the augmented-LLM enhancements above, and one of AWS’s core architecture components (AWS).

Multi-agent systems. Instead of one agent doing everything, you can have several specialized agents collaborate. Both the classical taxonomy and AWS list multi-agent systems as a category (Wikipedia; AWS). Anthropic’s orchestrator-workers pattern is a concrete version: a central LLM breaks a task into subtasks, delegates them, and synthesizes the results (Anthropic).

A quick note on “types of agents”

If you read the academic material, you’ll see five classical classes: simple reflex, model-based reflex, goal-based, utility-based, and learning agents (Wikipedia). AWS extends the list with hierarchical and multi-agent systems (AWS). You don’t need to memorize these, but they’re a useful reminder that “agent” is a spectrum of autonomy, not a single thing.

When to use an agent — and when not to

This is the section I wish more people read first. The honest guidance from Anthropic is to resist building an agent until you actually need one: “find the simplest solution possible, and only increasing complexity when needed.” Agentic systems “typically increase latency and costs while improving task performance,” so it’s a genuine trade-off, not a free upgrade. In fact, for many applications, “optimizing single LLM calls with retrieval and in-context examples is usually enough” (Anthropic).

A practical way to decide:

  • Use a workflow when the task is well-defined and you value predictability. If you can draw the flowchart yourself, hard-code it.
  • Use an agent when flexibility and model-driven decision-making are essential — the path can’t be known in advance, and the number of possible steps is large (Anthropic).

Common patterns that sit between a single prompt and a full agent are worth knowing, because they often solve the problem more cheaply: prompt chaining (sequential steps with checkpoints), routing (classify an input and send it to a specialist), parallelization (split work up, or run it several times and vote), orchestrator-workers, and evaluator-optimizer (one model generates, another critiques, in a loop) (Anthropic). My advice: try to solve your problem with one of these before you reach for full autonomy.

Keeping costs under control

Because agents run the LLM in a loop and call tools repeatedly, cost and latency are design concerns from day one, not afterthoughts. The trade-off is baked in — more autonomy means more model calls (Anthropic). Grounded in the sources above, here’s how to keep the bill sane:

  • Don’t build an agent you don’t need. The cheapest agent is the one you avoided by using a single well-designed LLM call, a retrieval step, or a fixed workflow instead (Anthropic).
  • Set stopping conditions. Anthropic explicitly recommends limits like a maximum number of iterations so an agent can’t loop forever and quietly run up cost (Anthropic). This is the single most important cost guardrail.
  • Load only what’s needed. This is exactly why progressive disclosure in Skills matters — it “only loads what’s needed, when it’s needed” instead of stuffing every instruction into context on every call (Anthropic). Less context per call means lower token cost.
  • Match the pattern to the task. A routing step that sends easy requests down a cheaper path, and only hard ones to the expensive full-agent path, can cut cost dramatically (Anthropic).

A caveat, so I don’t overstate things: specific pricing, token rates, and model-tier costs change constantly and depend on your provider. Check the current pricing page for whatever model you use — I’m deliberately not quoting numbers the sources didn’t give me.

Security, guardrails, and human oversight

Handing an autonomous system access to your tools and data is a real risk surface, and the sources are consistent about how to manage it: test in isolation, constrain what the agent can do, and keep a human in the loop.

  • Test in a sandbox. Anthropic calls “extensive testing in sandboxed environments” essential before you let an agent loose (Anthropic). Give it a safe playground before it touches production.
  • Design tools to prevent mistakes. The poka-yoke principle again — make dangerous or ambiguous tool use structurally hard, not just discouraged (Anthropic).
  • Use platform guardrails. Managed services provide built-in safety layers; AWS points to “Amazon Bedrock Guardrails” as an example of built-in security for agents (AWS).
  • Keep humans in the loop. Agents should be able to pause for human feedback “at checkpoints or when blocked” (Anthropic), and human review remains a core safeguard against biased or inaccurate output (AWS). For anything irreversible — sending money, deleting data, emailing customers — a human approval step is not optional.

If you take one security principle away: an agent should never have a capability you wouldn’t hand to a brand-new employee on their first day without supervision.

Bringing it together

Here’s the whole picture in one view:

  • An AI agent takes a goal in plain language and pursues it self-directedly.
  • Under the hood it’s an augmented LLM (model + tools + memory + retrieval) running a perceive → reason → act → observe loop.
  • Skills package reusable expertise; MCP standardizes how the agent connects to the outside world; multi-agent setups split work across specialists.
  • Reach for an agent only when a simpler workflow won’t do — autonomy costs latency and money.
  • Stopping conditions, sandboxing, and human oversight are what make the whole thing safe to run.

The technology is genuinely useful, but the teams who succeed with it are the ones who stay skeptical: they start simple, add autonomy only where it earns its keep, and never let an agent act unsupervised on anything that’s hard to undo.


References

  1. Anthropic — Building Effective Agents
  2. AWS — What are AI agents?
  3. Anthropic — Agent Skills
  4. Model Context Protocol — Introduction
  5. Wikipedia — Intelligent agent