Skip to content

AWS dynamic dispatcher with transient secrets - #9316

Open
allisonkarlitskaya wants to merge 11 commits into
mainfrom
ec3-thingy
Open

AWS dynamic dispatcher with transient secrets#9316
allisonkarlitskaya wants to merge 11 commits into
mainfrom
ec3-thingy

Conversation

@allisonkarlitskaya

@allisonkarlitskaya allisonkarlitskaya commented Aug 4, 2026

Copy link
Copy Markdown
Member

This is a massive change. It moves us to being able to dispatch jobs on AWS, but that undersells it substantially.

The three main benefits here (and the resultant complexity) are:

  • dynamic job configuration to move us away from static secrets (although GitHub is still an issue)
  • dynamic runner dispatch to allow ramping up more runners in time of high demand and to avoid runners sitting idle when there's no jobs (which is most of the time)
  • infrastructure-as-code in the bots repository so we can deploy everything in a few seconds. No more Ansible (and long wait times) for the dispatcher/runners.

The lib/aws/README.md file is probably a good place to start reading.

Comment thread lib/aws/ensure_resource.py Fixed
Comment thread lib/aws/infractl.py
print(f" - bots ref: {args.bots_ref}")
print(f" - bots sha: {sha}")
print(f" - bots url: {bots_url}")
print(f" - secrets: {list(secrets)}")
Comment thread lib/aws/jobconfig.py
duration: timedelta = timedelta(hours=1),
) -> Iterator[tuple[str, JsonValue]]:

logger.debug("providing secrets %r with duration %r", secrets, duration)
Comment thread saml-login
match output:
case 'env':
print(f'export AWS_ACCESS_KEY_ID={shlex.quote(key.access)}')
print(f'export AWS_SECRET_ACCESS_KEY={shlex.quote(key.secret)}')
Comment thread saml-login
Comment on lines +51 to +56
print(json.dumps({
'Version': 1,
'AccessKeyId': key.access,
'SecretAccessKey': key.secret,
'SessionToken': key.token,
}))
There's no real decent upstream stubs for pika but they're kinda small
so we can do our own.
This file defines various constants pertaining to our AWS account.
Users of it will be introduced in the following commits.

In general things in this directory will need to run on our future
dispatcher, which will be running Debian trixie, which only has Python
3.13.  Add a ruff override.
In 7c6e4af ("sts: increase ticket duration to 12 hours") we
increased the session duration to 12 hours for all roles, but that
information is actually tied to the specific role and most of them don't
support longer sessions.

Move that to to the SAMLTarget dataclass and take the chance to use the
various constants from `lib/aws/accounts.py`.
Move secret expansion out of JobContext.__aenter__() and into a new
prepare_secrets() method that is called from run_container() with the
job's requested secrets and a tmpdir for materialized inline values.

This means container.secrets entries in the config are only expanded
when a job actually requests them — secrets that no job asks for sit
inert, even if their %{name} references can't be resolved.  This
unblocks putting secret wiring in the default config without it
exploding for unconfigured secrets.

The two error cases (no wiring for a requested secret, wiring
references an undefined value) raise LookupError with a descriptive
message, caught in run_container() and surfaced as a job Failure.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Our log bucket and image download stores are now entirely derivable from
our AWS account information, so let's do that.

We still keep this file separate: these used to come from Linode and may
well some day be replaced (or supplemented) by non-AWS things again.
This will help us serialize minted S3 keys when spinning up instances in
the new dispatcher.
Add an option to attach the journal output from the current systemd
invocation to the log generated by job-runner.  This can be helpful for
showing how job-runner itself was invoked, assuming it's done via a
one-off unit.
Modify `saml-login` to use the constants from `lib/aws/accounts.py`
instead of selecting the first target in `gssapi_saml_sts.py` (which is
now defined in terms of the former, anyway).

Add the ability to authenticate as arbitrary roles by passing the role
name as a positional on the command line.  For compatibility, the
default is still to authenticate for downloading CI images.

Add a new --output= CLI argument which allows generating a POSIX shell
block (`--output=env`) which can be sourced to set `AWS_*` environment
variables.  Even better is a `--output=awscreds` which can be used as a
credential helper in `~/.aws/credentials`, like so:

```
[admin]
credential_process = /path/to/saml-login --output=awscreds 727920394381-admin
```
This is the start of a centralized definition of the shape of our CI
infrastructure inside of AWS.  It replaces and expands on the existing
`ansible/aws/setup-s3-buckets.yml` in the `cockpituous` repository by
adding the IAM roles for SSO-authenticated `image-download` and the
roles that will be used by the new task dispatcher.  It also provisions
an ASG (autoscaling group) which ensures that a single machine is always
running to function as the dispatcher.  Currently no actual dispatcher
is running in that machine, but the roles are configured and we can use
it for testing.

The contents of this directory use the `boto3` Python library (the
official AWS API for Python) and assume that you're authenticated
against a suitable IAM Role.  Further automation around this point will
come soon.

The main entry point here is an `infractl` command which has three
subcommands:

 - `sync`: ensures all of our deployed resources match the configuration
   defined in `infra_definitions.py` and reports on any resources which
   aren't managed in that file

 - `dispatcher`: allows querying the status of the dispatcher,
   temporarily taking it offline, restarting it, and has an `ssh`
   command to ssh to it

 - `runners`: lists current CI runs in progress

The "meat" of the infrastructure definition is inside of
`lib/aws/infra_definitions.py` which is written in an imperative style
but can effectively be treated as declarative data.  It's backstopped by
a series of functions in `ensure_resource.py` which are about ensuring
that the resource in question has the desired shape.  I considered a
very large number of alternative approaches before, during, and after
developing this but all of the existing solutions have a number of
problems:

 - depending on "community" modules for with incomplete support of the
   resources we're deploying (Ansible)

 - needing to keep a browser open with documentation in order to write
   yaml dialects (Ansible, CloudFormation) with poor editor support or
   custom DSLs (Terraform/OpenTofu).

 - requirement for extra "state" storage volumes and/or databases for
   locking (Terraform/OpenTofu)

 - "state" that is used as the basis for reconciliation but can itself
   differ from reality when "emergency fixes" are made in the AWS web
   console (Terraform/OpenTofu/CF)

 - no tracking of resources outside of what is mentioned in the plan or
   the state file (all)

Our own custom-rolled solution has its own drawbacks, mainly that we
have to maintain the helpers in `ensure_resource.py`, and trust that they
produce correct results.  It also doesn't have support for dry-runs or
diffing.  On the other hand, it's written in Python (understood by
everyone on the team) and the type annotations and editor support are
extremely good, even down to detecting typos in instance types and
region names.

The closest contenders were probably AWS CDK and the unified AWS Cloud
Control API.  AWS CDK is similar to what we're doing and has a nice
`cdk` CLI tool for deployment but is a massive unpackaged (on Fedora)
dependency which is a somewhat lossy Python binding wrapped around a
Node.js core with a bridge between them.  Cloud control is interesting
but somewhat unwieldy due to the massive number of sub-resource types
and the need to understand JSON schemas and produce JSON merge patches
to interact with it properly.  It also doesn't give a huge advantage
over the "traditional" APIs we're already using.
This script has been useful for me when creating test runs using a local
AMQP server.

In general, you can do something like this:

  podman run --rm -it -p 5672:5672 docker.io/library/rabbitmq
  python3 -m test.generate_test_jobs --amqp > slugs
  python3 -m lib.aws.dispatcher --param amqp-server=localhost:5672

and then inspect the generated logs for failures.  `slugs` will contain
a list of the relevant slugs on the logs server.
The dashboard html and JS have been written against and checked with
Biome and TypeScript.  Let's add configs for those (and stubs for Lit)
and wire them up to test/run.
) -> None:
ssm = boto3.client("ssm", region_name=CI_RUNNER_REGION)

print(f" - {name} ({param_type}, {len(value)} bytes)")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants