The AWS Cross-Account Access Pattern That Replaced Our Entire Service Mesh (When You Do Not Need…
IAM role assumption, resource policies, and PrivateLink already solve the problems you installed Istio for. Here is when to rip it out.
The AWS Cross-Account Access Pattern That Replaced Our Entire Service Mesh (When You Do Not Need Istio)
IAM role assumption, resource policies, and PrivateLink already solve the problems you installed Istio for. Here is when to rip it out.
It is 2 AM. PagerDuty fires. Your service mesh sidecar injection webhook is failing, and every new pod in your cluster starts without network connectivity. You spend 45 minutes debugging Istio’s MutatingWebhookConfiguration while your actual application code sits idle, working perfectly. For our team, the answer turned out to be: nothing that AWS did not already handle.
The Mesh We Thought We Needed
We had 14 services across 6 AWS accounts. They talked to each other through SQS, SNS, Lambda invocations, and API Gateway. The communication patterns were not complicated. Service A publishes to a topic, services B and C subscribe. Service D calls service E through a private API. Standard AWS-native architecture that had worked fine for two years.
Then we adopted Istio. Not because we had a specific problem it solved, but because zero-trust networking was the industry default and our security review mentioned “service-to-service authentication” as a gap. Istio was the obvious answer. Everyone was using it.
Six months later, I looked at what we actually used from the mesh: mTLS between pods and access logging. That was it. We never configured traffic shaping. We never did canary routing. We deployed an entire service mesh control plane and injected Envoy sidecars into every pod for two features that AWS gives you differently but equivalently.
The cost was not theoretical. The Envoy sidecars consumed 1.8GB of RAM across the cluster. Istio’s performance docs cite roughly 2.5ms of added p99 latency per hop and 50 to 128MB per sidecar instance. Our control plane used more CPU than three of our actual production services. Every Istio minor version upgrade was a project. We had CRD conflicts twice during upgrades that took the entire mesh offline for 20 minutes each time.
The moment that changed my thinking was an incident where a misconfigured PeerAuthentication resource silently broke cross-namespace communication. We spent four hours debugging it. The fix was a one-line YAML change. I realized we had added an entire infrastructure layer — with its own failure modes and operational burden — to get capabilities that the underlying AWS platform already provided through IAM, resource policies, and encryption in transit.
What a Service Mesh Actually Gives You
A service mesh gives you four things: workload identity with mTLS, L7 observability, traffic management, and authorization policies. The problem is that most teams adopt a mesh for one or two of these capabilities but pay the full operational cost of all four.
Workload identity is the headline feature. Istio issues SPIFFE certificates to every workload, and sidecars enforce mutual TLS on every hop. Authorization policies let you write rules like “service A can call service B on path /api/orders with GET only.” These two capabilities are what drew us in.
But a mesh also gives you per-request metrics, distributed tracing headers, canary routing with weighted splits, circuit breaking, retries with budgets, and fault injection. We used almost none of this. Our canary deployments ran through CodeDeploy. Our retries lived in application code. Our tracing came from X-Ray. We were running an entire Envoy data plane to get identity and authorization.
I needed mTLS and authorization. I did not need a traffic management layer. Once I separated those requirements clearly, the AWS-native alternative became obvious.
The AWS Services You Already Pay For

Diagram: Mapping of service mesh capabilities to their AWS-native equivalents, showing which managed service replaces each mesh feature.
Here is the thing that frustrated me most: every capability I needed was already running in my AWS accounts. I was paying for it. I just had not mapped the pieces together.
IAM is your policy engine. Identity-based policies define what a service can do. Resource-based policies define who can access a specific resource. Together, they give you fine-grained authorization at the API level — per-action, per-resource, with conditions on source account, organization ID, VPC endpoint, or time of day. Istio’s AuthorizationPolicy does the same thing, except you maintain a control plane to enforce it.
STS is your certificate authority. When Service A assumes a role in Account B, STS issues temporary credentials with a configurable TTL between 15 minutes and 12 hours. Every request signed with those credentials uses SigV4, a cryptographic signature over the request headers, body, and timestamp. The credentials rotate automatically. There is no certificate renewal process, no expired cert at 3 AM. This is what mTLS gives you in a mesh, except AWS manages the entire lifecycle.
CloudTrail is your access log. Every AssumeRole call, every cross-account API invocation gets logged with the caller’s identity, the target resource, timestamp, source IP, and user agent. You can aggregate these into an organization trail across all accounts. CloudTrail was already recording everything I needed Envoy access logs for.
PrivateLink is your network isolation layer. When you create a VPC endpoint for a service in another account, traffic stays on the AWS backbone network and never touches the public internet. You can attach endpoint policies to restrict which principals can use the endpoint and which actions they can perform. Note that PrivateLink provides network-path isolation — TLS encryption is enforced by the service endpoints themselves (all AWS service APIs require TLS 1.2+), not by PrivateLink directly. If you are fronting your own services with PrivateLink, you are still responsible for TLS termination on your end.
AWS Organizations SCPs set the maximum permissions boundary for every account in your organization. Even if someone misconfigures an IAM role to be too permissive, the SCP prevents actions outside the allowed set. This is your safety net — the equivalent of a mesh-wide deny policy that no individual service can override.
Five services. All managed. All already in my bill.
The Cross-Account Pattern in Practice
The whole pattern fits in four pieces, and every piece is a JSON document you commit to version control. No daemons. No sidecars. No control plane.
First, the trust policy on the target role. This is the gate that decides who can assume the role:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {"aws:PrincipalOrgID": "o-abc123def4"}
}
}]
}
The aws:PrincipalOrgID condition is the key. Setting Principal to “*” with an org condition means only principals from your AWS Organization can assume this role — no explicit account lists to maintain, no external IDs needed for internal services. When you add a new account to your org, it works automatically.
Second, the calling service assumes the role with a short session:
sts = boto3.client("sts")
creds = sts.assume_role(
RoleArn="arn:aws:iam::222222222222:role/orders-read",
RoleSessionName="payments-service",
DurationSeconds=900
)
Fifteen minutes. If those credentials leak, they are useless in sixteen minutes. The session name shows up in CloudTrail, so you know exactly which service made which call. For long-running services, use boto3’s RefreshableCredentials to handle automatic renewal before expiration.
Third, a resource policy on the target adds a second authorization layer:
{
"Effect": "Allow",
"Principal": {"AWS": "*"},
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:222222222222:order-events",
"Condition": {
"StringEquals": {"aws:PrincipalOrgID": "o-abc123def4"},
"ArnLike": {"aws:PrincipalArn": "arn:aws:iam::*:role/payments-*"}
}
}
The ArnLike condition restricts access to roles matching a naming pattern. Even if someone in your org assumes a different role in the target account, they cannot touch this queue unless their role name starts with payments-. Defense in depth, no runtime component.
Fourth, VPC endpoint policies restrict network-level access. Even if a compromised workload in a different account somehow gets valid credentials, it cannot reach the SQS endpoint from its VPC if the endpoint policy does not allow it.
All four pieces are Terraform resources or CDK constructs. They live in your repository, go through code review, and deploy through your existing pipeline. The entire authorization layer is declarative JSON that AWS evaluates at request time with zero additional infrastructure on your side.
What We Gained by Removing Istio
The first thing we noticed was not performance. It was the on-call pager going quiet on a specific category of incidents. Sidecar injection failures, Istio control plane OOM kills, CRD version skew after partial upgrades — these incidents simply stopped existing.
Pod startup time dropped by 3–5 seconds across our services. The istio-init container and the Envoy sidecar were gone, which meant cold starts became predictable again. Our cluster-wide memory usage dropped by roughly 1.8GB and freed about 2 CPU cores — enough to delay our next node group scale-up by months.
We eliminated quarterly Istio upgrades entirely. Between 2022 and 2023, Envoy accumulated over 15 CVEs requiring patches. Each patch meant testing the new sidecar image against every service, rolling restarts across environments, and hoping nothing broke in the mTLS handshake. That maintenance tax is now zero. AWS patches their own infrastructure without our involvement.
Debugging got dramatically simpler. When a service call fails now, the path is: our code, the AWS SDK, the AWS API. There is no Envoy proxy in between silently retrying, applying circuit breakers we forgot we configured, or returning a 503 that looks like our service is down when actually the sidecar lost connection to the control plane. Engineers stopped asking “is this a mesh problem or an application problem?”
The audit story improved in a way I did not anticipate. CloudTrail captures every cross-account AssumeRole call automatically — who assumed what role, from which source, at what time. This trail is immutable and requires zero collection infrastructure from us. We replaced a system we had to operate with a system AWS operates for us.
When You Still Need a Service Mesh
I am not arguing that service meshes are bad technology. I am arguing that many teams adopt them for problems they do not actually have.
If your primary concern is L7 observability — per-request metrics, latency percentile breakdowns at the route level — IAM role assumption gives you nothing. CloudTrail tells you who called what, but it does not tell you that /api/orders has a p99 of 340ms while /api/orders/{id}/items sits at 12ms. If you need the depth that Envoy’s tap filters provide, you need a mesh.
Traffic shaping has no IAM equivalent. Canary routing, fault injection, retry budgets, circuit breaking at the network layer — these are capabilities that live in the data plane. If you need to shift 5% of traffic to a new version while watching error rates, you need something between your pods and the network.
If your services communicate over raw gRPC or TCP between pods — not through AWS APIs — then the cross-account pattern simply does not apply. Pod-to-pod communication within a cluster needs its own identity and encryption story, and mTLS via a mesh is the standard answer.
Multi-cloud or hybrid environments cannot rely on AWS-native identity. If part of your fleet runs on GCP or on-premises, IAM is not a universal identity provider.
There is also a scale boundary. Past roughly 30 services, IAM policy management becomes its own operational burden. The concrete wall you hit: managed IAM policies cap at 6,144 characters for inline policies and 10,240 characters for managed policies. Trust policies allow a maximum of 50 principals. Condition keys multiply combinatorially as service pairs grow. At that point, Istio’s AuthorizationPolicy CRDs might actually be easier to reason about than a web of IAM JSON documents.
I should also acknowledge that Istio’s ambient mesh mode significantly weakens the “sidecar tax” argument. No more per-pod proxy containers eating memory and adding latency. If ambient mesh had existed three years ago, my cost-benefit calculation would have been closer.
The decision comes down to your primary concern. If it is authorization and auditability for AWS-native services, IAM wins on operational simplicity. If it is observability, traffic management, or non-AWS communication, a mesh earns its complexity.
The Decision Framework

Diagram: The decision framework for choosing between AWS-native cross-account patterns and a service mesh based on actual requirements.
Here is the mental model I use now when someone asks whether they need a service mesh.
If your services talk to each other through AWS APIs — SQS, SNS, Lambda invocations, API Gateway — you do not need a mesh. IAM already handles authentication, authorization, and audit for every call. Adding Istio on top means you are paying for a second authorization layer that does less than the first.
If your primary concern is “who can call what” and “can I prove it in an audit,” use IAM with cross-account role assumption. If your primary concern is request-level observability, traffic splitting, or communication with non-AWS services, a mesh might earn its keep. These are different problems, and the tooling should match.
The pattern I described works when you meet three conditions: you are single-cloud AWS, you have fewer than 20–30 services, and your inter-service communication uses AWS APIs or PrivateLink. Once you exceed that boundary — multi-cloud, heavy gRPC between pods, complex traffic shaping — a mesh starts solving problems that IAM cannot.
For canary deployments specifically, you do not need Istio. ALB weighted target groups, CodeDeploy traffic shifting, and Route 53 weighted routing all give you percentage-based rollouts without a sidecar fleet. On EKS, Pod Identity gives you the same cross-account assumption pattern natively inside Kubernetes.
The sharpest test: if you adopted Istio but only use mTLS and access logging, you are running an entire control plane and sidecar fleet for two features that AWS provides natively. PrivateLink isolates the network path. Service endpoints enforce TLS. CloudTrail logs every call. That is not simplification through abstraction — that is complexity for its own sake.
The Migration Was Three Weeks, Not Three Months
I expected this to take a quarter. It took three weeks. The technical migration was not the hard part — convincing the team was.
Week one was pure Terraform. We created cross-account IAM roles with trust policies scoped to aws:PrincipalOrgID and source VPC endpoints for all 14 services. One module, parameterized per service pair. The entire PR was about 400 lines of HCL.
Week two was service code changes and network plumbing. We replaced mesh-authenticated calls with STS AssumeRole, which gave us short-lived credentials signed with SigV4 instead of mTLS certificates managed by Istio’s control plane. We added VPC endpoints with endpoint policies that restricted which principals could traverse them.
Week three was the careful part. We disabled sidecar injection one namespace at a time, starting with the lowest-traffic internal service. After each namespace, we watched CloudTrail for 48 hours to confirm access patterns matched what we expected. Our rollback plan was trivial: re-enable sidecar injection on the namespace label. We never needed it.
The hardest conversation was not technical. It was explaining to the team that removing infrastructure is also engineering work. Running a control plane you do not need is not engineering maturity — it is sunk cost fallacy with a monthly bill.
What I Would Tell You Before You Install Istio
Here is my decision criteria, and I wish someone had given it to me three years ago. Start with cross-account IAM role assumption and PrivateLink. Run that pattern until you hit a wall that only a mesh can solve. Those walls are specific: you need L7 traffic shaping (canary routing by header, fault injection), you need protocol-level observability beyond what CloudWatch provides, or you need identity federation across cloud providers. If none of those describe your situation today, you do not need a mesh today.
Most teams I talk to want auth and audit. They want to know which service called which, they want to block unauthorized calls, and they want a log of everything. IAM already does all three. STS gives you cryptographically signed short-lived credentials. Resource policies give you fine-grained authorization. CloudTrail gives you a complete audit trail. You are not missing a service mesh — you are not recognizing the one AWS already ships.
The cost of Istio is not the afternoon you spend installing it. It is the next two years of control plane upgrades, Envoy CVE patches, and debugging sidecar injection failures at 2 AM. You need IAM, resource policies, and the discipline to not install infrastructure because it looks impressive on an architecture diagram.
메타데이터
- post_id
- 534db2edd752
- slug
- the-aws-cross-account-access-pattern-that-replaced-our-entire-service-mesh-when-you-do-not-need-534db2edd752
- url
- https://medium.com/@yalovoy/the-aws-cross-account-access-pattern-that-replaced-our-entire-service-mesh-when-you-do-not-need-534db2edd752
- canonical_url
- https://medium.com/@yalovoy/the-aws-cross-account-access-pattern-that-replaced-our-entire-service-mesh-when-you-do-not-need-534db2edd752
- author_url
- https://medium.com/@yalovoy
- status
- ok
- fetched_at
- 2026-06-16 19:09:56