@0x5278/blastradius
v0.1.1
Published
Find what a terraform plan actually breaks: BFS the plan's own dependency graph past the resources being changed.
Maintainers
Readme
blastradius
terraform plan tells you what it's changing. It doesn't tell you what
breaks as a result. blastradius reads the plan's own dependency graph and
BFS-traces every resource downstream of a delete/replace/update - including
resources Terraform marks no-op, which can still fail at runtime when
something they depend on vanishes.
- DELETE aws_security_group.db_sg (blast risk: 18)
└─ aws_db_instance.main [depth 1, risk 9]
└─ aws_db_instance.replica [depth 2, risk 9]That depth-2 catch - the read replica nobody was thinking about - is the
entire point. terraform plan is depth-0 by design; it only lists what
changes, not what breaks as a consequence.
Install
npm install -g @0x5278/blastradius
# or just: npx @0x5278/blastradius --plan plan.jsonUsage
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
blastradius --plan plan.json # human-readable CLI report
blastradius --plan plan.json --format markdown # for PR comments
blastradius --plan plan.json --format json # for scripting
# or pipe directly, no intermediate file
terraform show -json tfplan | blastradiusExit code is non-zero when any change has a totalRisk >= 20 (default
threshold, override with --threshold), so it gates CI out of the box.
In CI
- run: terraform plan -out=tfplan
- run: terraform show -json tfplan > plan.json
- run: npx @0x5278/blastradius --plan plan.json --format markdown >> $GITHUB_STEP_SUMMARY
- run: npx @0x5278/blastradius --plan plan.json # exits 1 if totalRisk >= 20PR reviewers see the risk table before approving, not after the incident.
As a library
const { run, formatReport } = require('@0x5278/blastradius');
const results = run(planJsonString, { minSeverity: 'update' });
console.log(formatReport(results, 'cli'));results is an array, one entry per qualifying change:
{
address: 'aws_security_group.db_sg',
type: 'aws_security_group',
severity: 'delete',
totalRisk: 18,
affected: [
{ address: 'aws_db_instance.main', type: 'aws_db_instance', depth: 1, risk: 9 },
{ address: 'aws_db_instance.replica', type: 'aws_db_instance', depth: 2, risk: 9 },
],
}Options:
minSeverity: skip changes below this severity (create,update,replace,delete). Default:create.includeNoOps: include changes whose only action isno-op(can still fail at runtime when a dependency is deleted). Default:false.
How it works
lib/parser.js validate plan JSON, extract resource_changes + configuration
lib/graph.js walk expressions[].references recursively -> reverse dependency graph
lib/analyzer.js BFS from each change over the reverse graph, score by resource type
lib/report.js cli / json / markdown formatters
bin/cli.js stdin or --plan, exits non-zero above the risk floor
main.js library entry pointThe reference graph isn't reconstructed from HCL - Terraform's own plan JSON
already contains it under configuration.root_module.resources[].expressions.*.references.
blastradius walks data that is already there.
Module calls are recursed too: configuration.root_module.module_calls[name].module
is walked with address flattening, so module.ec2.aws_security_group.sg and
outputs (module.ec2.main_arn) resolve correctly across module boundaries.
Risk scoring is a static weight table per resource type, summed over the affected dependents. It is deliberately a guess; the long-term plan is to train these weights off a team's own incident history.
Try it
git clone https://github.com/JoeyKoch1/blastradius.git
cd blastradius
npm install
npm run exampleRuns against examples/example-plan.json - a security group deletion that
cascades through an RDS instance to a read replica two hops away - and
examples/example-module-plan.json, the same scenario with the replica
reached through a module boundary.
Known limitations / roadmap
for_each/countaddresses are normalized (index suffixes are stripped when matching references), but the index itself is not derived from expressions.- Risk weights are hardcoded guesses, not learned from anything. The real long-term differentiator is training these off a team's own incident history instead of a static table.
- No cross-state support. Graphs that span
terraform_remote_stateboundaries are not stitched together.
License
MIT - see LICENSE.
