Architecture¶
Module Overview¶
InfraHouse Core is organized into two main areas:
aws – AWS service wrappers with consistent session, credential, and role-assumption handling.
github – GitHub Actions runner management via the GitHub API.
Supporting modules provide logging, timeouts, filesystem helpers, and input validation.
infrahouse_core/
├── aws/
│ ├── __init__.py # get_session, get_client, get_resource, SSO login
│ ├── base.py # AWSResource base class
│ ├── ec2_instance.py # EC2 + SSM command execution
│ ├── dynamodb.py # DynamoDB table + distributed locking
│ ├── secretsmanager.py # Secrets Manager wrapper
│ ├── route53/zone.py # DNS record management
│ ├── config.py # ~/.aws/config parser
│ └── ... # ACM, CloudFront, ECS, IAM, S3, etc.
├── orchestrator/ # RAFT cluster coordination
├── github.py # GitHub Actions runner management
├── logging.py # stdout/stderr log routing
├── timeout.py # SIGALRM-based timeout context manager
└── validation.py # Input validation helpers
Session, Client, and Resource Functions¶
The aws module provides three functions for obtaining boto3 objects.
All three accept optional role_arn and region parameters.
get_session(role_arn=None, region=None)Returns a
boto3.Session, optionally with assumed-role credentials. Use this when you need to create multiple clients or resources from the same set of credentials.get_client(service_name, role_arn=None, region=None)Convenience wrapper – creates a session and returns a single low-level client (e.g.
get_client("ec2")).get_resource(service_name, role_arn=None, region=None)Convenience wrapper – creates a session and returns a single high-level resource (e.g.
get_resource("dynamodb")).
When to use which:
Use
get_client(the most common case) when you need one service client.Use
get_resourcewhen you want the higher-level boto3 resource API (DynamoDB Table, S3 Bucket, etc.).Use
get_sessionwhen you need to create multiple clients/resources sharing the same assumed-role credentials.
Example:
from infrahouse_core.aws import get_client, get_resource, get_session
# Single client
ec2 = get_client("ec2", region="us-east-1")
# Single resource
table = get_resource("dynamodb").Table("my-table")
# Shared session for cross-account access
session = get_session(role_arn="arn:aws:iam::123456789012:role/MyRole")
ec2 = session.client("ec2")
ssm = session.client("ssm")
Role Assumption¶
Cross-account (or cross-role) access is supported throughout the library
via an optional role_arn parameter.
At the function level:
client = get_client("s3", role_arn="arn:aws:iam::123456789012:role/S3Reader")
At the class level:
Most resource classes (EC2Instance, DynamoDBTable, Secret, Zone, etc.)
accept role_arn in their constructors. The role is assumed lazily when the
first API call is made:
from infrahouse_core.aws.secretsmanager import Secret
# Read a secret from another account
secret = Secret("my-secret", role_arn="arn:aws:iam::123456789012:role/SecretsReader")
print(secret.value)
Session names are auto-generated from the caller’s module and function name
for CloudTrail auditing. You can override this with the session_name parameter.
GitHub Runner Iteration¶
GitHubActions.runners and GitHubActions.find_runners_by_label() return
iterators, not lists. They stream pages from the GitHub API lazily and yield
one GitHubActionsRunner at a time, so memory usage stays bounded to a
single page (~100 runners) regardless of organization size.
This matters in memory-constrained environments — for example, a 128 MB AWS
Lambda function running against a large organization would previously OOM
because runners materialized the entire list before returning. With the
iterator contract, find_runner_by_label() also short-circuits: once a match
is found on the current page, subsequent pages are never fetched.
Callers that need a materialized collection should wrap the result with
list():
all_runners = list(gha.runners)
alphas = list(gha.find_runners_by_label("alpha"))
Typical lazy usage:
for runner in gha.runners:
if runner.busy:
continue
gha.deregister_runner(runner)
Deregistering while iterating is safe: deregister_runner issues an
independent DELETE /orgs/{org}/actions/runners/{id} request and does not
mutate the paginated list response that the iterator is currently reading
from. Contrast this with iterating a local list and mutating it in place,
which Python forbids — the GitHub pagination cursor lives server-side and
is advanced only when the iterator asks for the next page.
Caching Behaviour¶
Several classes use cached_property_with_ttl to avoid redundant API calls.
The default TTL is 10 seconds – after that the property is re-fetched on
next access.
Properties that use TTL caching include:
EC2Instance._describe_instance(instance metadata)GitHubActionsRunner._runner_data(runner status)
This means rapid successive reads (e.g. checking instance.state twice in a
tight loop) hit the cache, but reads separated by more than 10 seconds always
get fresh data.
Client Lifecycle¶
Resource classes create their boto3 clients lazily on first use and reuse them for the lifetime of the object. This means:
Constructing a class is cheap – no API calls are made.
The first property access or method call creates the client.
Subsequent calls reuse the same client.
If you need to refresh credentials (e.g. after a long-running process), create a new instance of the class.
AWSResource Base Class¶
All AWS resource wrapper classes follow the contract defined by AWSResource:
existsproperty – returnsTrueif the resource currently exists.delete()method – idempotent deletion (no error if already absent).delete()uses EAFP (try/except), not check-then-act, to avoid TOCTOU race conditions.