Skip to content
Linuxbeast
  • Home
  • Today in Tech
  • Who is Hiring?
  • About Me
  • Work With Me
  • Lab
    • DevOps Onboarding
    • Tag Network
    • Tools
      • VPC Subnet Planner
      • Loan Calculator
  • Contact
How to Deploy a Cross-Account IAM Role with CloudFormation StackSets

How to Deploy a Cross-Account IAM Role with CloudFormation StackSets

May 2, 2026 by Linuxbeast
5 min read
16 views

If you run a multi-account AWS Organization, you have probably hit this problem: a tooling service in one account needs read-only access to resources in every other account. Doing that by hand means logging into each account, creating the same IAM role, and praying you do not typo the trust policy on account number 23. CloudFormation StackSets solve this — one template, deployed across an entire Organizational Unit (OU), in one command.

This guide walks you through a real cross-account discovery setup I deployed a few weeks ago: a CloudFormation StackSet that creates a read-only IAM role and enables AWS Resource Explorer 2 in every member account, all driven from the Organization management account. The examples are run on WSL2 Ubuntu in Windows, but they work the same on any Ubuntu system.

What a StackSet Actually Does

A StackSet is a CloudFormation template plus a list of target accounts and regions. CloudFormation creates one stack instance per account-region pair, all from a single source of truth. Update the template once, and every member stack picks up the change.

In this example, each target account ends up with two things:

  • A read-only IAM role (CrossAccountDiscoveryRole) that a tooling account can assume
  • An AWS Resource Explorer 2 index so the Search API actually returns results

Prerequisites

  • An AWS Organization with at least two accounts (a management account and one or more member accounts)
  • StackSets enabled in service-managed mode (the management account permission setup is done once via AWS Organizations)
  • AWS CLI v2 installed and configured — see How to Install AWS CLI v2 on Ubuntu 22.04
  • SSO access to the management account — if you have not set that up yet, follow How to Configure AWS SSO CLI Access for Linux Ubuntu
  • A basic understanding of cross-account IAM trust — covered in How to Set Up Cross-Account Access in AWS with AssumeRole

The CloudFormation Template

Save this as cross-account-discovery.yaml. It is parameterized so you do not have to hardcode account IDs or role patterns into the template.

AWSTemplateFormatVersion: '2010-09-09'
Description: Cross-account discovery role + Resource Explorer 2 index.

Parameters:
  TrustedAccountId:
    Type: String
    AllowedPattern: '^\d{12}$'
    Description: Account ID where the tooling service runs.

  TrustedRolePattern:
    Type: String
    Default: 'ecs-task-role-*'
    Description: Role name pattern allowed to assume the discovery role.

  ResourceExplorerIndexType:
    Type: String
    Default: LOCAL
    AllowedValues: [LOCAL, AGGREGATOR]

  CreateResourceExplorerIndex:
    Type: String
    Default: 'true'
    AllowedValues: ['true', 'false']

Conditions:
  ShouldCreateIndex: !Equals [!Ref CreateResourceExplorerIndex, 'true']

Resources:
  CrossAccountDiscoveryRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: CrossAccountDiscoveryRole
      MaxSessionDuration: 3600
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${TrustedAccountId}:root'
            Action: sts:AssumeRole
            Condition:
              StringLike:
                'aws:PrincipalArn': !Sub 'arn:aws:iam::${TrustedAccountId}:role/${TrustedRolePattern}'
      Policies:
        - PolicyName: ResourceExplorerReadOnly
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - resource-explorer-2:Search
                  - resource-explorer-2:GetView
                  - resource-explorer-2:ListViews
                  - resource-explorer-2:ListIndexes
                  - resource-explorer-2:GetIndex
                  - tag:GetResources
                Resource: '*'

  ResourceExplorerIndex:
    Type: AWS::ResourceExplorer2::Index
    Condition: ShouldCreateIndex
    Properties:
      Type: !Ref ResourceExplorerIndexType
      Tags:
        managed-by: stackset

Outputs:
  RoleArn:
    Value: !GetAtt CrossAccountDiscoveryRole.Arn

A few things worth pointing out:

  • The trust policy uses a StringLike condition on aws:PrincipalArn. Without it, any principal in the trusted account can assume the role. With it, only roles matching the pattern (e.g., ecs-task-role-*) can.
  • All actions are read-only. The tag:GetResources permission requires Resource: '*' because the Resource Groups Tagging API does not support resource-level permissions.
  • CreateResourceExplorerIndex is a flag because some accounts may already have a Resource Explorer index — creating a second one will fail the stack.

Step-by-Step Guide

1. Log in to the management account

StackSets that target an OU must be created from the Organization management account.

aws sso login --profile org-master.admin
aws sts get-caller-identity --profile org-master.admin

Replace org-master.admin with whatever profile name you use for the management account.

2. Validate the template

aws cloudformation validate-template \
  --template-body file://cross-account-discovery.yaml \
  --profile org-master.admin

If the template is valid, you will see the parameters and capabilities echoed back. If not, fix the YAML before going any further.

3. Create the StackSet

aws cloudformation create-stack-set \
  --stack-set-name CrossAccountDiscoveryRole \
  --template-body file://cross-account-discovery.yaml \
  --parameters \
    ParameterKey=TrustedAccountId,ParameterValue=123456789012 \
    ParameterKey=TrustedRolePattern,ParameterValue=ecs-task-role-* \
    ParameterKey=ResourceExplorerIndexType,ParameterValue=LOCAL \
    ParameterKey=CreateResourceExplorerIndex,ParameterValue=true \
  --capabilities CAPABILITY_NAMED_IAM \
  --permission-model SERVICE_MANAGED \
  --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false \
  --profile org-master.admin
  • SERVICE_MANAGED permission model — uses Organizations-managed IAM roles instead of self-managed ones
  • --auto-deployment Enabled=true — when a new account joins a target OU, the stack is created automatically
  • CAPABILITY_NAMED_IAM is required because we set a fixed RoleName
  • Replace 123456789012 with the AWS account ID of your tooling account

4. Deploy to an OU

Creating the StackSet does not deploy anything yet. You need stack instances, scoped to one or more OUs.

aws cloudformation create-stack-instances \
  --stack-set-name CrossAccountDiscoveryRole \
  --deployment-targets OrganizationalUnitIds=ou-xxxx-aaaaaaaa \
  --regions us-east-1 \
  --operation-preferences FailureTolerancePercentage=50,MaxConcurrentPercentage=100 \
  --profile org-master.admin
  • ou-xxxx-aaaaaaaa — replace with your actual OU ID (find it via aws organizations list-organizational-units-for-parent)
  • FailureTolerancePercentage=50 — keep going even if half the accounts fail; useful when you have one weird account that needs special handling
  • MaxConcurrentPercentage=100 — fan out to all accounts at once

5. Watch the rollout

aws cloudformation list-stack-instances \
  --stack-set-name CrossAccountDiscoveryRole \
  --query 'Summaries[*].{Account:Account,Status:StackInstanceStatus.DetailedStatus}' \
  --profile org-master.admin

You will see a row per account with statuses like PENDING, RUNNING, SUCCEEDED, or FAILED. Most stacks finish in 1–3 minutes.

6. Update the template later

This is where StackSets really pay off. Edit the YAML, then push the change to every account in one command:

aws cloudformation update-stack-set \
  --stack-set-name CrossAccountDiscoveryRole \
  --template-body file://cross-account-discovery.yaml \
  --parameters \
    ParameterKey=TrustedAccountId,UsePreviousValue=true \
    ParameterKey=TrustedRolePattern,UsePreviousValue=true \
    ParameterKey=ResourceExplorerIndexType,UsePreviousValue=true \
    ParameterKey=CreateResourceExplorerIndex,UsePreviousValue=true \
  --capabilities CAPABILITY_NAMED_IAM \
  --profile org-master.admin

UsePreviousValue=true tells CloudFormation to reuse the parameter value from the existing stack set, so you do not need to repeat the account ID every time.

Common Gotchas

Symptom Cause
Index already exists in some accounts Resource Explorer was enabled previously. Set CreateResourceExplorerIndex=false for those accounts via stack instance overrides.
AccessDenied from the management account Service-managed StackSets need trusted access enabled in Organizations. Run aws organizations enable-aws-service-access --service-principal stacksets.cloudformation.amazonaws.com once.
Role created but assume role fails The aws:PrincipalArn condition pattern does not match. Confirm the actual role name in the tooling account matches the pattern.
Drift in some accounts Someone edited the role manually. Run detect-stack-set-drift to find them, then re-deploy.

Why This Beats Per-Account Setup

  • One source of truth. The role definition lives in Git as a CloudFormation template. No drift unless someone goes out of band.
  • Auto-deploy on new accounts. When a new account is added to a target OU, the role and index appear without anyone touching it.
  • Safe rollback. Update the StackSet with the previous template version, and every account rolls back together.
  • Audit trail. Every change is a CloudFormation event, visible in CloudTrail.

Conclusion

StackSets turn a tedious “log in to 30 accounts and click around” job into a single CLI command, with auto-deployment for any future accounts you add to the OU. If your tooling needs cross-account read access, this is the path of least pain.

From here, you might want to read How to Copy S3 Bucket Objects Across AWS Accounts for a different cross-account use case, or How to Access AWS Secrets Manager from Another Account if your tooling also needs cross-account secrets.

Categories AWS Tags AssumeRole, AWS Organizations, CloudFormation, Cross-Account, IAM, Resource Explorer, StackSets
How to Run AWS Serverless Locally with DynamoDB Local and LocalStack
← PreviousHow to Run AWS Serverless Locally with DynamoDB Local and LocalStack

Related Articles

How to Sync HubSpot Company Records to S3 with AWS Lambda and Step Functions
AWS

How to Sync HubSpot Company Records to S3 with AWS Lambda and Step Functions

How to Extract S3 Payload from SQS + SNS Event in AWS Lambda
AWS

How to Extract S3 Payload from SQS + SNS Event in AWS Lambda

How to Automate MySQL Database Backups on EC2 to Amazon S3
AWS

How to Automate MySQL Database Backups on EC2 to Amazon S3

LinuxBeast

Built for DevOps, Cloud & Automation.

Explore

  • Home
  • Today in Tech
  • About Me
  • Work With Me

Resources

  • Who is Hiring?
  • Lab
  • Contact

Legal

  • Privacy Policy
  • Cookie Policy
© 2026 Linuxbeast · All rights reserved.
© 2026 Linuxbeast · All rights reserved.
Manage Consent
To provide the best experiences, we use technologies like cookies to store and/or access device information. Consenting to these technologies will allow us to process data such as browsing behavior or unique IDs on this site. Not consenting or withdrawing consent, may adversely affect certain features and functions.
Functional Always active
The technical storage or access is strictly necessary for the legitimate purpose of enabling the use of a specific service explicitly requested by the subscriber or user, or for the sole purpose of carrying out the transmission of a communication over an electronic communications network.
Preferences
The technical storage or access is necessary for the legitimate purpose of storing preferences that are not requested by the subscriber or user.
Statistics
The technical storage or access that is used exclusively for statistical purposes. The technical storage or access that is used exclusively for anonymous statistical purposes. Without a subpoena, voluntary compliance on the part of your Internet Service Provider, or additional records from a third party, information stored or retrieved for this purpose alone cannot usually be used to identify you.
Marketing
The technical storage or access is required to create user profiles to send advertising, or to track the user on a website or across several websites for similar marketing purposes.
  • Manage options
  • Manage services
  • Manage {vendor_count} vendors
  • Read more about these purposes
View preferences
  • {title}
  • {title}
  • {title}