Skip to content

Building simplified cross-account role management for AWS Organizations

6 minute read
Content level: Intermediate
0

This article shows how to build a lightweight automation solution that transforms account access management for AWS Organizations that use AWS Identity and Access Management (IAM) users.

Introduction

Enterprises that manage hundreds of AWS accounts often face the challenge of maintaining all the account IDs and role names across their documentation sources. These sources can include wiki pages, spreadsheets, and other team documents. To address this issue, AWS Support developed a solution while working with an Enterprise Support customer to reduce operational overhead and accelerate developer productivity. In this article, you will learn how to build a lightweight automation solution that transforms account access management for AWS Organizations with IAM users. The solution generates an automated HTML dashboard that provides one-click role switching to reduce manual lookup and access errors. As seen in Figure 1, the solution uses Amazon EventBridge, AWS Lambda, and Amazon Simple Storage Service (Amazon S3) to house the dashboard.

Enter image description here

Figure 1: How users access the dashboard.

Solution overview

The solution consists of the following components, as seen in Figure 2:

  • Amazon EventBridge Scheduler to refresh the website at a set interval.
  • A Lambda function that uses AWS Organizations to generate the website, and then uploads the site to an Amazon S3 bucket.
  • An S3 bucket for users to access the dashboard.

Enter image description here

Figure 2: Overview of the solution.

Solution implementation

To implement the solution, complete the following steps.

Prerequisites

  • An AWS account with AWS Organizations turned on
  • An S3 bucket to host the HTML file
  • A cross-account IAM role, such as a role named OpsAccess

Create an IAM role and attach a policy

Create an IAM role for the Lambda function and attach a policy based on least privilege access. Example policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "OrganizationsReadAccess",
      "Effect": "Allow",
      "Action": [
        "organizations:ListAccounts"
      ],
      "Resource": "*"
    },
    {
      "Sid": "S3WriteAccess",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::S3_BUCKET/aws-accounts.html"
    },
    {
      "Sid": "CloudWatchLogsAccess",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    }
  ]
}

Note: Replace S3_BUCKET with the name of your S3 bucket.

Create a Lambda function

Create a Python-based Lambda function. Configure the function to use the IAM role that you created, and then increase the Lambda function timeout configuration to 60 seconds.

In the following example, the cross-account role name is OpsAccess, the S3 bucket name is easy-account-access-website, and the website header is AWS Accounts List. Replace these variables with your information.

Example:

import json
import boto3
from datetime import datetime

# Variable for the third column
crossAccountRoleName = "OpsAccess"
s3Bucket = "easy-account-access-website"
websiteHeader = "AWS Accounts List"

def lambda_handler(event, context):
    # Create boto3 clients
    org = boto3.client('organizations')
    s3 = boto3.client('s3')
    
    # Get S3 bucket name from environment variable or use a default
    # You'll need to set this environment variable for your Lambda function
    s3_bucket = event.get('bucket_name', s3Bucket)
    
    # Get the current date and time for the filename
    current_time = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
    s3_key = f"aws-accounts.html"
    
    # List AWS accounts
    accounts = []
    paginator = org.get_paginator('list_accounts')
    
    # Use pagination to handle large numbers of accounts
    for page in paginator.paginate():
        for account in page['Accounts']:
            accounts.append({
                'name': account['Name'],
                'id': account['Id']
            })
    
    # Generate HTML content
    html_content = generate_html(accounts, crossAccountRoleName)
    
    # Upload HTML to S3
    s3.put_object(
        Bucket=s3_bucket,
        Key=s3_key,
        Body=html_content,
        ContentType='text/html'
    )
    
    # Get the S3 URL for the uploaded file
    s3_url = f"https://{s3_bucket}.s3.amazonaws.com/%7Bs3_key%7D"
    
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': 'HTML file generated and uploaded successfully!',
            'accounts_processed': len(accounts),
            'file_url': s3_url
        })
    }

def generate_html(accounts, role_name):
    # HTML header with some basic styling
    html = """
    <!DOCTYPE html>
    <html>
    <head>
        <title>""" + websiteHeader + """</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 20px; }
            table { border-collapse: collapse; width: 100%; }
            th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
            th { background-color: #f2f2f2; }
            tr:nth-child(even) { background-color: #f9f9f9; }
            .header { background-color: #232F3E; color: white; padding: 10px; margin-bottom: 20px; }
        </style>
    </head>
    <body>
        <div class="header">
            <h1>""" + websiteHeader + """</h1>
            <p>Generated on: """ + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + """</p>
        </div>
        <table>
            <tr>
                <th>Account Name</th>
                <th>Account ID</th>
                <th>Role Name</th>
            </tr>
    """
    
    # Add a row for each account
    for account in accounts:
        html += f"""
            <tr>
                <td>{account['name']}</td>
                <td>{account['id']}</td>
                <td><a href="https://signin.aws.amazon.com/switchrole?account={account['id']}&roleName={crossAccountRoleName}">{role_name}</a></td>
            </tr>
        """
    
    # Close the HTML tags
    html += """
        </table>
    </body>
    </html>
    """
    
    return html

Example website hosted on an S3 bucket

As seen in Figure 3, the solution displays the account information, including the Account Name, Account ID, and Role Name.

Enter image description here

Figure 3: Static website for the solution. For best practices on how to host a secure static website on Amazon S3, see Get started with a secure static website.

Conclusion

As organizations continue to accelerate their cloud adoption, multi-account AWS architectures have become the standard. Regardless of the industry that the organization operates in, the challenge that they face is the same: Tracking multiple AWS accounts and the roles that grant access to them. This solution addresses that universal pain point with a lightweight, automated approach that can scale with your organization in AWS Organizations.

By combining AWS Organizations APIs with a serverless architecture, you reduce the friction of manual account lookups and access errors that slow down daily operations.

This solution provides the following benefits for organizations:

  • Operational efficiency: Developers can switch accounts in seconds rather than minutes, with pre-populated role details that reduce copy errors.
  • Minimal infrastructure: Three AWS services (EventBridge, Lambda, and Amazon S3) deliver an enterprise-scale account management solution without complex tooling.
  • Immediate return on investment (ROI): The solution pays for itself through reduced support tickets and faster incident response.
    As AWS Organizations continue to grow in complexity, automated solutions become essential infrastructure rather than a convenient tool.

For organizations that run at scale, Enterprise Support can further enhance this workflow. Your Technical Account Manager (TAM) can help with the following:

  • Review cross-account access architecture
  • Compare IAM role configurations against AWS best practices
  • Proactively identify access-related risks

AWS Trusted Advisor checks can also surface unused roles or overly permissive policies across your accounts.

About the author

Enter image description here

Rajat Antil
Rajat Antil is a Technical Consultant at AWS with experience in Cloud infrastructure, operations, and automation. A passionate technology enthusiast, he enjoys solving complex problems, automating mundane tasks, and building solutions for large scale enterprises that are secure, scalable, and resilient.

Enter image description here

Anant Jain
Anant is a Technical Consultant at AWS specializing in storage, resilience, and networking. He architects data protection and migration solutions for large-scale enterprise customers, helping them build secure, highly available cloud infrastructures. As a trusted advisor, he partners with organizations to design tailored cloud strategies that align technical solutions with business objectives.