Skip to main content

RunAcross

CI PyPI

Concurrent Python execution across AWS accounts.

RunAcross handles STS AssumeRole, concurrent execution, error isolation and result aggregation so your code can focus on the AWS operation itself.

RunAcross 0.1 executes across accounts. Account-by-Region execution is planned for a later release.

Why RunAcross?

Multi-account scripts repeatedly need the same plumbing:

accounts
-> STS AssumeRole
-> boto3 Session
-> ThreadPoolExecutor
-> callback
-> isolated errors
-> aggregated results

RunAcross packages that pattern as a small synchronous function. It is a library primitive, not a scanner, CLI, scheduler, or credentials manager.

Installation

pip install runacross

RunAcross requires Python 3.10 or later. For development from a local clone:

python -m pip install -e ".[dev]"

Quickstart

from runacross import map_accounts


def who_am_i(session, account):
    sts = session.client("sts")
    return sts.get_caller_identity()["Arn"]


results = map_accounts(
    who_am_i,
    accounts=[
        "111111111111",
        "222222222222",
    ],
    role_name="SecurityAuditRole",
    max_workers=10,
)

for result in results:
    if result.success:
        print(f"{result.account.id}: {result.value}")
    else:
        print(f"{result.account.id}: {result.phase}: {result.error}")

Account IDs are converted to immutable Account objects. You can also provide metadata explicitly:

from runacross import Account

accounts = [
    Account(id="111111111111", name="Production"),
    Account(id="222222222222", name="Development"),
]

Using AWS Organizations

Organizations is an optional, explicit source of accounts:

from runacross import map_accounts
from runacross.organizations import list_accounts

accounts = list_accounts(
    organization_id="o-exampleorgid",
    exclude_accounts=["111111111111"],
)

results = map_accounts(
    who_am_i,
    accounts=accounts,
    role_name="SecurityAuditRole",
)

The organization ID is a safety check, not a selector. AWS uses the source credentials to determine which organization is visible. RunAcross verifies that it matches the expected ID and then returns accounts whose current Organizations State is ACTIVE.

Call list_accounts() without an ID when that guard is not needed. Discovered Account objects include the Organizations name and root email address; treat those fields as sensitive. Account redacts the email in repr() output, but the value remains on the object.

See examples/organization_accounts.py for a complete discovery-plus-execution script.

Handling failures

One failed account does not cancel the others:

for result in results:
    if result.success:
        use(result.value)
    elif result.phase == "assume_role":
        report_access_problem(result.account, result.error)
    else:
        report_worker_problem(result.account, result.error)

RunResults preserves input order and provides:

results.successful
results.failed
results.success_count
results.failure_count

Use result.unwrap() when code wants the typed value or the stored exception:

for result in results:
    try:
        value = result.unwrap()
    except Exception as error:
        handle(result.account, error)

RunAcross does not retry the callback because arbitrary functions may not be idempotent.

Concurrency

RunAcross uses ThreadPoolExecutor, which is suitable for the network-bound work performed by Boto3. The default is 10 workers:

results = map_accounts(
    who_am_i,
    accounts=accounts,
    role_name="SecurityAuditRole",
    max_workers=5,
)

More workers do not imply linear speedups. High concurrency can increase throttling, connection use, memory use, and Lambda duration.

RunAcross-owned clients use Botocore standard retries with three total attempts. Override them explicitly when needed:

from botocore.config import Config

results = map_accounts(
    who_am_i,
    accounts=accounts,
    role_name="SecurityAuditRole",
    botocore_config=Config(
        retries={"mode": "adaptive", "total_max_attempts": 5},
    ),
)

This configuration applies only to clients RunAcross creates, such as STS. Pass a Config to clients created inside your callback to configure their retries.

Authentication

By default, RunAcross uses boto3.Session() and the standard Boto3 credential provider chain. It works with configured environment credentials, profiles, IAM Identity Center, web identity, ECS task roles, EC2 instance profiles, Lambda execution roles, and GitHub Actions OIDC.

You can provide a source Session:

import boto3

source_session = boto3.Session(profile_name="security")

results = map_accounts(
    who_am_i,
    accounts=accounts,
    role_name="SecurityAuditRole",
    source_session=source_session,
)

The source Session must have a Region. RunAcross copies it onto each assumed Session and raises ValueError before calling STS if it is missing. Configure AWS_DEFAULT_REGION, the profile's region, or boto3.Session(region_name=...).

The target role must trust the source identity, and the source identity must be allowed to call sts:AssumeRole.

Assumed Sessions in 0.1 do not refresh automatically. Callbacks should finish within the STS session lifetime, which defaults to one hour. Pass duration_seconds (900-43200, still subject to the role maximum) to request a shorter or longer session:

results = map_accounts(
    who_am_i,
    accounts=accounts,
    role_name="SecurityAuditRole",
    duration_seconds=900,
)

IAM permissions

The source identity needs sts:AssumeRole for the target roles. Organizations discovery additionally needs organizations:ListAccounts; using the organization ID guard also needs organizations:DescribeOrganization.

The assumed role needs only the service permissions used by the callback. See docs/iam.md for restrictive examples and trust-policy requirements.

Running in AWS Lambda

Lambda execution-role credentials are discovered automatically. Package RunAcross and its Boto3 dependency with the function or in a layer so the versions are controlled by your deployment.

All callbacks must finish before the handler returns. Do not leave RunAcross work running in the background between Lambda invocations. Tune max_workers for the function's memory, timeout, and downstream AWS quotas.

Security

RunAcross does not persist or return STS credentials, add telemetry, or create non-AWS service clients. Library logging is silent unless the application configures it. RunAcross never deliberately adds credentials to logs, but callback exception messages are emitted at DEBUG and must not contain secrets.

See SECURITY.md for vulnerability reporting.

Roadmap

Planned areas include explicit account-by-Region execution, enabled-Region discovery, and small Organizations filters. RunAcross will remain a library primitive rather than becoming an orchestration framework.

See docs/roadmap.md.

Contributing

Development uses pytest, Ruff, and mypy. See CONTRIBUTING.md.

RunAcross is licensed under the Apache License 2.0.

Metadata

Release files for runacross 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for runacross 0.1.2
File Size Uploaded
runacross-0.1.2.tar.gz 29.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for runacross 0.1.2
File Interpreter ABI Platform
runacross-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 44.7 kB

Release files / runacross-0.1.2.tar.gz

Download URL runacross-0.1.2.tar.gz
Size 29.1 kB
Tags Source
SHA-256 checksum
How to use checksums
9dea12df63a6df662bc2333d37eafe8ccc11eaa96424b111ceac99018d2d7986
BLAKE2b-256 checksum
How to use checksums
2bbec33652eb09188b52ed82b3016f2078c5efba30f9d8e2bb1e1443939e424b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release files / runacross-0.1.2-py3-none-any.whl

Download URL runacross-0.1.2-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e165c20128a1678263f00c0b1ec0e9e5e2c273fc1d862ddffb3b20abdaa04d40
BLAKE2b-256 checksum
How to use checksums
d89d57fe988e8873b9f8c1715f2c10d59bccb755e17d67cfbb2d82725256f39a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 20, 2026.

Transparency log

Release history Release notifications | RSS feed

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.0

2 release files

This release

0.1.2 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page