Why
AWS is amazing, but it's hard to ship fast unless you're an expert.
I want to write down best practices as code, then forget about them so I can just ship.
I want to serve http, react to database writes, and use cron to schedule actions.
I want to push data S3 => EC2 => S3 with ephemeral Spot instances that live for seconds.
Did you know that EC2 is billed by the second, that Spot is 1/5 the price, and that Spot in Local Zones is 1/2 that price?
It's basically free. Oh and no egress fees between EC2 and S3 in the same region. Lambda's not the only thing that scales to zero. AWS is pretty awesome.
AWS should:
- Have fewer knobs
- Have sane defaults
- Be easy to use
- Be hard to screw up
- Be fast
- Be fun
- Have a tldr
It should be easy for a lambda to react to:
- Docker push to ecr
- S3 put object
- DynamoDB put item
- SQS send message
- Time passing
- http requests
- url streaming HTTP requests
- websocket messages
It should be easy to create:
How
Declare and deploy groups of related AWS infrastructure as infrastructure sets:
-
That contain:
- Lambdas
- S3 buckets
- IAM users
- DynamoDB tables
- SQS queues
- VPCs
- Security groups
- Instance profiles
- Keypairs
-
That react to Lambda triggers:
What
A simpler way to declare AWS infrastructure that is easy to use and extend.
There are two ways to use it:
-
Go structs and the Go API
The primary entrypoints are:
-
infra-ensure: deploy an infrastructure set.
libaws infra-ensure ./infra.yaml --preview libaws infra-ensure ./infra.yaml
-
infra-ls: view infrastructure sets.
-
infra-ensure --quick: quickly update Lambda code.
libaws infra-ensure ./infra.yaml --quick LAMBDA_NAME
-
infra-ensure --build-only / --from-build: build Lambda artifacts on one host, deploy them on another.
libaws infra-ensure ./infra.yaml --build-only ./build libaws infra-ensure ./infra.yaml --from-build ./build
-
infra-rm: remove an infrastructure set.
libaws infra-rm ./infra.yaml --preview libaws infra-rm ./infra.yaml
infra-ensure is a positive assertion. It asserts that some named infrastructure exists, and is configured correctly, creating or updating it if needed.
Other commands fall into two categories:
- Mutate AWS state:
ensure,new, andrm. - View AWS state:
ls,get,scan, anddescribe.
AWS SDK, Pulumi, Terraform, CloudFormation, and Serverless
Compared to the full AWS API, systems declared as infrastructure sets:
-
Are easier to use.
-
Are harder to screw up.
-
Are almost always enough, and easy to extend.
-
Are more fun.
If you want to use the full AWS API, there are many great tools:
Readme Index
Install
CLI
go install github.com/nathants/libaws@latest export PATH=$PATH:$(go env GOPATH)/bin
Go API
go get github.com/nathants/libaws@latest
TLDR
Define an Infrastructure Set
>> cd examples/simple/go/s3 && tree . ├── infra.yaml └── main.go
name: test-infraset-${uid} s3: test-bucket-${uid}: attr: - acl=private lambda: test-lambda-${uid}: entrypoint: main.go attr: - memory=128 - timeout=60 policy: - AWSLambdaBasicExecutionRole trigger: - type: s3 attr: - test-bucket-${uid}
package main import ( "context" "fmt" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) func handleRequest(_ context.Context, e events.S3Event) (events.APIGatewayProxyResponse, error) { for _, record := range e.Records { fmt.Println(record.S3.Object.Key) } return events.APIGatewayProxyResponse{StatusCode: 200}, nil } func main() { lambda.Start(handleRequest) }
Ensure the Infrastructure Set
View the Infrastructure Set
infra-ls lists the account. Select one exact set with --infraset NAME, or filter names with a positional substring; these selectors cannot be combined:
libaws infra-ls --infraset my-project
Depth-based colors by YAML
Trigger the Infrastructure Set
Quickly Update Lambda Code
Build and Deploy Lambda Artifacts Separately
Build the infrastructure set's Go Lambda code into a directory of artifacts, then deploy those exact bytes from the directory:
libaws infra-ensure ./infra.yaml --build-only ./build libaws infra-ensure ./infra.yaml --from-build ./build
--build-only writes each artifact and a manifest.json recording its name, file, sha256, and the entrypoint Git checkout's commit and dirty state. It never accesses AWS. --from-build uses only hash-verified artifacts: it never compiles, and it never reads Lambda entrypoint sources. Only Go Lambdas are built into artifacts; Python and container Lambdas are unaffected, and Python Lambdas still package from their entrypoint and requirements on the deploy host. A missing or mismatched artifact fails before any AWS call, as does --from-build when the selected infrastructure set needs no artifact. An empty directory value, a --from-build path without a manifest, and an unwritable --build-only directory are errors. Both flags also combine with --quick LAMBDA_NAME.
Delete the Infrastructure Set
Usage
Explore the CLI
Explore a CLI Entrypoint
For ec2-ssh, ec2-scp, ec2-rsync, and ec2-wait-ssh, -p/--preview only lists targets. Across the CLI, -p always means --preview; --private-ip, --profile, and --period have no short aliases.
ec2-rm-ami AMI_ID deregisters an owned AMI and asks EC2 to delete its associated snapshots, preserving snapshots used by other AMIs. --preview/-p lists verified backing snapshots as conditional deletions because sharing is checked at deletion time. Snapshot-deletion failures return an error even if deregistration succeeded; their snapshot IDs identify any remaining cleanup. An AMI ID that exists but is not owned by this account fails instead of being treated as absent.
Explore the Go API
Use editor completion, the API reference, or local documentation:
go doc github.com/nathants/libaws/lib go doc github.com/nathants/libaws/lib.InfraListSet
Explore Simple Examples
- Alarm: go
- API: python, go, docker, custom domain
- DynamoDB: python, go, docker
- EC2: CLI with Spot/on-demand launches, VPC-selected or explicit subnets, and scoped SSH readiness pruning; AMI cleanup with shared-snapshot preservation.
- ECR: python, go, docker
- Function URL: go
- Includes: python, go
- S3: python, go, docker, R2 CLI
- Schedule: python, go, docker
- SES: go
- SQS: python, go, docker
- Websocket: python, go, docker
Explore Complex Examples
-
Coexisting infrastructure sets: exact-set inventory while an independent set is being removed.
-
Literal S3 keys: object/version CLI operations without path normalization.
-
DynamoDB indexes: provisioned global indexes and local-index projections.
-
S3 append-only: append-only storage with separate writer and reader IAM users.
-
- Write to S3 in-bucket
- Which triggers Lambda
- Which launches EC2 Spot
- Which reads from in-bucket, writes to out-bucket, and terminates
Explore External Examples
Infrastructure Set
An infrastructure set is defined by YAML or Go struct and contains:
-
Stateful infrastructure:
-
EC2 infrastructure:
Typical Usage
-
Use infra-ensure to deploy an infrastructure set.
libaws infra-ensure ./infra.yaml --preview libaws infra-ensure ./infra.yaml
-
Use infra-ls to view infrastructure sets.
-
Use infra-ensure --quick LAMBDA_NAME to quickly update Lambda code.
libaws infra-ensure ./infra.yaml --quick LAMBDA_NAME
-
Use infra-rm to remove an infrastructure set.
libaws infra-rm ./infra.yaml --preview libaws infra-rm ./infra.yaml
Design
-
There is no implicit coordination.
- If you aren't already serializing your infrastructure mutations, lock around DynamoDB.
-
No databases for infrastructure state. There are only two state locations:
- AWS.
- Your code.
-
AWS infrastructure is uniquely identified by name.
- All AWS infrastructure share a private namespace scoped to account/region. Use good names.
- Except S3, which shares a public namespace scoped to Earth. Use better names.
-
Mutative operations manipulate AWS state.
- Mutative operations are idempotent. If they fail due to a transient error, run them again.
- Mutative operations can
--preview. No output means no changes.
-
ensureare mutative operations that create or update infrastructure. -
rmare mutative operations that delete infrastructure. -
ls,get,scan, anddescribeoperations are non-mutative. -
Multiple infrastructure sets can be deployed into the same account/region.
Tradeoffs
-
ensureoperations are positive assertions. They assert that some named infrastructure exists, and is configured correctly, creating or updating it if needed.-
When possible, an existing same-named resource is automatically adopted and converged to the requested configuration.
-
Positive assertions CANNOT remove top-level infrastructure, but CAN remove configuration from them.
-
Removing a
trigger,policy, orallowWILL remove that from thelambda. -
Removing
policy, orallowWILL remove that from theinstance-profile. -
Removing a
security-groupWILL remove that from thevpc. -
Removing a
ruleWILL remove that from thesecurity-group. -
Removing an
attrWILL remove that from asqs,s3,dynamodb, orlambda. -
Removing a
keypair,vpc,instance-profile,sqs,s3,dynamodb, orlambdaWON'T remove that from the account/region.-
The operator decides IF and WHEN top-level infrastructure should be deleted, then uses an
rmoperation to do so. -
As a convenience,
infra-rmwill remove ALL infrastructure CURRENTLY declared in aninfra.yaml. -
If a declared Lambda is already gone,
infra-rmremoves verifiably owned triggers and preserves ambiguous or independently owned resources.
-
-
-
When using
ensureoperations, no output means no changes.-
For large infrastructure sets, this can mean a minute or two without output if no changes are needed.
-
To see a lot of output instead of none, set this environment variable:
-
-
Since
ensureoperations are idempotent, if you encounter errors like rate limits, just try again. -
infra-lsis designed to list AWS accounts managed withinfra-ensure. It will not work well in other scenarios.
infra.yaml
Use an infra.yaml file to declare an infrastructure set. The schema is as follows:
name: VALUE lambda: VALUE: entrypoint: VALUE policy: [VALUE ...] allow: [VALUE ...] attr: [VALUE ...] require: [VALUE ...] env: [VALUE ...] include: [VALUE ...] trigger: - type: VALUE attr: [VALUE ...] s3: VALUE: attr: [VALUE ...] user: VALUE: allow: [VALUE ...] policy: [VALUE ...] dynamodb: VALUE: key: [VALUE ...] attr: [VALUE ...] sqs: VALUE: attr: [VALUE ...] vpc: VALUE: security-group: VALUE: rule: [VALUE ...] keypair: VALUE: pubkey-content: VALUE instance-profile: VALUE: allow: [VALUE ...] policy: [VALUE ...]
Environment Variable Substitution
Anywhere in infra.yaml you can substitute environment variables from the caller's environment:
-
Example:
s3: test-bucket-${uid}: attr: - versioning=${versioning}
The following variables are defined during deployment, and are useful in allow declarations:
-
${API_ID}the ID of the API Gateway v2 API created by anapitrigger. -
${WEBSOCKET_ID}the ID of the API Gateway v2 websocket created by awebsockettrigger.
Name
Defines the name of the infrastructure set.
-
Schema:
-
Example:
S3
Defines a S3 bucket:
-
The following attributes can be defined:
acl=VALUE, values:public | private, default:privateversioning=VALUE, values:true | false, default:falseappendonly=VALUE, values:true | false, default:falsemetrics=VALUE, values:true | false, default:falsecors=VALUE, values:true | false, default:falsettldays=VALUE, values:0 | n, default:0. Bucket-wide expiration in days.allow_put=VALUE, values:$principal.amazonaws.com
-
Managed buckets require TLS, use SSE-S3 encryption, and reject SSE-C.
-
S3 CLI paths preserve literal keys, including dot segments and repeated slashes;
s3-ls --quietprints fullbucket/keypaths. -
appendonly=truerequiresIf-None-Match: *for object creation and multipart completion, denies deletion, and cannot be combined with expiration. -
Setting
cors=trueuses*for allowed origins. To specify one or more explicit origins, do this instead:corsorigin=http://localhost:8080corsorigin=https://example.com
-
Bucket ACL can only be set at creation. Restore missing public-access-block settings before re-ensuring a bucket.
-
Schema:
-
Example:
s3: test-bucket: attr: - versioning=true - acl=public
IAM user
Defines an IAM user without creating credentials.
-
allowentries useSERVICE:ACTION RESOURCE. -
policyentries name existing managed policies. -
Undeclared inline and attached policies are removed.
-
Create the user's sole access key explicitly with
libaws iam-ensure-user-api-key USER. -
Schema:
user: VALUE: allow: - SERVICE:ACTION RESOURCE policy: - VALUE
-
Example:
user: backup-writer: allow: - s3:PutObject arn:aws:s3:::backup-bucket/*
DynamoDB
Defines a DynamoDB table:
-
Specify key as:
NAME:ATTR_TYPE:KEY_TYPE
-
The following attributes can be defined:
read=VALUE, provisioned read capacity, default:0write=VALUE, provisioned write capacity, default:0ttl=ATTR_NAME, optional expiration attribute.
-
On global indices the following attributes can be defined:
projection=VALUE, projection type, default:ALLread=VALUE, provisioned read capacity, default:0write=VALUE, provisioned write capacity, default:0
-
On local indices the following attributes can be defined:
projection=VALUE, projection type, default:ALL
-
Schema:
dynamodb: VALUE: key: - NAME:ATTR_TYPE:KEY_TYPE attr: - VALUE global-index: VALUE: key: - NAME:ATTR_TYPE:KEY_TYPE non-key: - NAME attr: - VALUE local-index: VALUE: key: - NAME:ATTR_TYPE:KEY_TYPE non-key: - NAME attr: - VALUE
-
Example:
dynamodb: stream-table: key: - userid:s:hash - timestamp:n:range attr: - stream=keys_only auth-table: key: - id:s:hash attr: - write=50 - read=150
-
Example global secondary index:
dynamodb: test-table: key: - id:s:hash global-index: test-index: key: - hometown:s:hash
-
Example local secondary index:
dynamodb: test-table: key: - id:s:hash - created:n:range local-index: test-index: key: - id:s:hash - hometown:s:range
SQS
Defines a SQS queue:
-
The following attributes can be defined:
delay=VALUE, delay seconds, default:0size=VALUE, maximum message size bytes, default:1048576retention=VALUE, message retention period seconds, default:345600wait=VALUE, receive wait time seconds, default:0timeout=VALUE, visibility timeout seconds, default:30
-
Schema:
sqs: VALUE: attr: - VALUE
-
Example:
sqs: test-queue: attr: - delay=20 - timeout=300
Keypair
Defines an EC2 keypair.
-
Schema:
keypair: VALUE: pubkey-content: VALUE
-
Example:
keypair: test-keypair: pubkey-content: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICVp11Z99AySWfbLrMBewZluh7cwLlkjifGH5u22RXor
VPC
Defines a default-like VPC with an Internet Gateway and public access.
ec2-new --vpc NAME_OR_ID selects subnets in zones offering --type: all matching VPC subnets for --spot, or one at random for on-demand. Explicit --subnets "subnet-A subnet-B" overrides this selection.
Before --init, the first detected NVMe instance-store disk is formatted as whole-device ext4 and mounted at /mnt, writable by the login user. EBS disks are excluded. Existing partitions, signatures, device use, an occupied /mnt, or failed safety reads abort initialization. Without instance store, no disk changes are made. The mount is recorded by filesystem UUID.
-
Schema:
-
Example:
Security Group
Defines a security group on a VPC.
-
Schema:
vpc: VALUE: security-group: VALUE: rule: - PROTO:PORT:SOURCE
-
Example:
vpc: test-vpc: security-group: test-sg: rule: - tcp:22:0.0.0.0/0
Instance Profile
Defines an EC2 instance profile.
-
Schema:
instance-profile: VALUE: allow: - SERVICE:ACTION ARN policy: - VALUE
-
Example:
instance-profile: test-profile: allow: - s3:* * policy: - AWSLambdaBasicExecutionRole
Lambda
Defines a Lambda.
-
Schema:
-
Example:
Entrypoint
Defines the code of the Lambda. It is one of:
-
A Python file.
-
A Go file. Libaws builds its complete package.
-
An ECR container URI ending in
@sha256:followed by 64 lowercase hexadecimal characters. -
Schema:
lambda: VALUE: entrypoint: VALUE
-
Example:
lambda: test-lambda: entrypoint: main.go
Attr
Defines Lambda attributes. The following can be defined:
-
concurrency=VALUE, reserved concurrency; omit for the unreserved pool or use0to disable the function. -
memory=VALUE, RAM in megabytes, default:128 -
timeout=VALUE, timeout in seconds, default:300 -
logs-ttl-days=VALUE, CloudWatch Logs retention in days;0disables expiration, default:7 -
Schema:
lambda: VALUE: attr: - KEY=VALUE
-
Example:
lambda: test-lambda: attr: - concurrency=100 - memory=256 - timeout=60 - logs-ttl-days=1
Policy
Defines policies on the Lambda's IAM role.
-
Schema:
lambda: VALUE: policy: - VALUE
-
Example:
lambda: test-lambda: policy: - AWSLambdaBasicExecutionRole
Allow
Defines allows on the Lambda's IAM role.
-
Schema:
lambda: VALUE: allow: - SERVICE:ACTION ARN
-
Example:
lambda: test-lambda: allow: - s3:* * - dynamodb:* arn:aws:dynamodb:*:*:table/test-table
Env
Defines environment variables on the Lambda:
-
Names must match
[a-zA-Z][a-zA-Z0-9_]+and may not be repeated. -
The environment JSON is limited to 4 KiB; the complete configuration update to 5 KiB.
-
Values may be empty and are preserved exactly.
-
Schema:
lambda: VALUE: env: - KEY=VALUE
-
Example:
lambda: test-lambda: env: - kind=production
Include
Defines extra content to include in the Lambda zip. Relative paths resolve beside infra.yaml:
-
This is ignored when
entrypointis an ECR container URI. -
Schema:
lambda: VALUE: include: - VALUE
-
Example:
lambda: test-lambda: include: - ./cacerts.crt - ../frontend/public/*
Require
Defines dependencies to install with pip in the virtualenv zip.
-
This is ignored unless the
entrypointis a Python file. -
Relative paths, including
-rrequirements.txtand local packages, resolve from the directory containinginfra.yaml. -
For source dependencies, add
--build-constraint=build-requirements.txtwith exact build-backend pins. -
Schema:
lambda: VALUE: require: - VALUE
-
Example:
lambda: test-lambda: require: - fastapi==0.76.0
Trigger
Defines triggers for the Lambda:
-
Schema:
lambda: VALUE: trigger: - type: VALUE attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: dynamodb attr: - test-table - start=latest
Trigger Types
SES
Defines an SES email trigger.
-
Required attrs:
dns=DOMAINandbucket=BUCKET; optional:prefix=PREFIX. -
Route53 and SES must already be configured for the domain, and the bucket must allow puts from SES.
-
The domain must be ASCII and at most 64 characters because it names the SES rule set.
-
Each infrastructure set may declare one SES trigger. Ensuring it replaces the active regional rule set.
-
Ensure rejects a same-named rule set containing other rules.
-
Schema:
lambda: VALUE: trigger: - type: ses attr: - VALUE
-
Example:
s3: my-bucket: attr: - allow_put=ses.amazonaws.com lambda: test-lambda: trigger: - type: ses attr: - dns=my-email-domain.com - bucket=my-bucket - prefix=emails/
API
Defines an API Gateway v2 HTTP API:
-
Add a custom domain with attr:
domain=api.example.com -
Add a custom domain and update Route53 with attr:
dns=api.example.com -
infra-rmremoves exclusively managed domains and aliases; shared domains, routing-rule domains, hosted zones, and certificates remain. -
Schema:
lambda: VALUE: trigger: - type: api attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: api attr: - dns=api.example.com
URL
Defines a Lambda function URL trigger with streaming HTTP responses.
-
No attributes are required.
-
Schema:
lambda: VALUE: trigger: - type: url
-
Example:
lambda: test-lambda: trigger: - type: url
Websocket
Defines an API Gateway v2 websocket API:
-
Add a custom domain with attr:
domain=ws.example.com -
Add a custom domain and update Route53 with attr:
dns=ws.example.com-
This domain, or its parent domain, must already exist as a hosted zone in route53-ls.
-
An exact or matching wildcard ACM certificate must already exist.
-
-
infra-rmremoves exclusively managed domains and aliases; shared domains, routing-rule domains, hosted zones, and certificates remain. -
Schema:
lambda: VALUE: trigger: - type: websocket attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: websocket attr: - dns=ws.example.com
S3
Defines an S3 trigger:
-
The only attribute must be a nonempty bucket name.
-
Object creation and deletion invoke the trigger.
-
Ensure rejects an existing conflicting notification instead of creating duplicate invocations.
-
Schema:
lambda: VALUE: trigger: - type: s3 attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: s3 attr: - test-bucket
DynamoDB
Defines a DynamoDB trigger:
-
The first attribute must be the table name.
-
The following trigger attributes can be defined:
batch=VALUE, maximum batch size, default:100parallel=VALUE, parallelization factor, default:1retry=VALUE, maximum retry attempts, default:-1window=VALUE, maximum batching window in seconds, default:0start=VALUE, required starting position:latest | trim_horizon; immutable on existing mappings.
-
Schema:
lambda: VALUE: trigger: - type: dynamodb attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: dynamodb attr: - test-table - start=trim_horizon
SQS
Defines a SQS trigger. Ensuring a declared trigger re-enables a disabled mapping:
-
The first attribute must be a nonempty queue name.
-
The following trigger attributes can be defined:
batch=VALUE, maximum batch size, default:10window=VALUE, maximum batching window in seconds, default:0
-
Schema:
lambda: VALUE: trigger: - type: sqs attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: sqs attr: - test-queue
Schedule
Defines a schedule trigger:
-
The only attribute must be the schedule expression.
-
Schema:
lambda: VALUE: trigger: - type: schedule attr: - VALUE
-
Example:
lambda: test-lambda: trigger: - type: schedule attr: - rate(24 hours)
ECR
Defines an ECR trigger:
-
Successful image actions from any ECR repository invoke the trigger; delivery is best-effort.
-
Schema:
lambda: VALUE: trigger: - type: ecr
-
Example:
lambda: test-lambda: trigger: - type: ecr
Alarm
Defines a CloudWatch alarm that invokes the declaring Lambda when the monitored Lambda records at least the specified number of invocations in one minute.
-
The following attributes are required:
name=VALUE, account-and-region-unique alarm name, 1–255 ASCII letters, digits, dots, hyphens, or underscores.lambda-invocations=VALUE, Lambda to monitor.at-least=VALUE/minute, positive integer invocation threshold, maximum:2147483647.
-
Missing data does not breach the alarm.
-
Schema:
lambda: VALUE: trigger: - type: alarm attr: - name=VALUE - lambda-invocations=VALUE - at-least=VALUE/minute
-
Example:
lambda: alert-handler: trigger: - type: alarm attr: - name=beta-invocations-runaway - lambda-invocations=beta - at-least=300/minute
Bash Completion
source completions.d/libaws.sh
Extending
Drop down to the AWS Go SDK and implement what you need.
Extend an existing mutative operation or add a new one.
- Make sure that mutative operations are IDEMPOTENT and can be PREVIEWED.
You will find examples in cmd/ and lib/ that can provide a good place to start.
You can reuse many existing operations like:
Alternatively, lift and shift to other infrastructure automation tooling. ls and describe operations will give you all the information you need.
Testing
Tests require Go 1.27+, Python 3.12+, uv, rootless Docker, and the check tools. Use scratch-account credentials and a permanent, publicly delegated Route53 zone in that account:
export LIBAWS_TEST_ACCOUNT=$ACCOUNT_NUM export LIBAWS_TEST_DOMAIN=scratch.example.com make test
Set LIBAWS_TEST_JOBS to change concurrency (default: 4; 1 for sequential runs). Do not overlap live suites in one account.
Tests retain shared certificate, DNS, and API-domain fixtures; see testing guidance for details.
R2 tests are opt-in: set LIBAWS_R2_TEST_ACCOUNT to your R2_ACCOUNT_ID and supply R2_ACCESS_KEY_ID and R2_ACCESS_KEY_SECRET. They create and remove unique libaws-testing-* buckets.
Run one example with:
make export PATH="$PWD:$PATH" cd examples/simple/python/api uv run --locked python -u test.py




