<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Scott Burgholzer</title>
    <description>The latest articles on DEV Community by Scott Burgholzer (@scottburgholzer).</description>
    <link>https://dev.to/scottburgholzer</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1330421%2F4d92c203-ead3-4683-a414-01b6a4e9c1de.jpg</url>
      <title>DEV Community: Scott Burgholzer</title>
      <link>https://dev.to/scottburgholzer</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scottburgholzer"/>
    <language>en</language>
    <item>
      <title>Serverless IaC Risk Analysis: The Architecture Behind Blast Radius</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Tue, 15 Sep 2026 19:40:50 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-iac-risk-analysis-the-architecture-behind-blast-radius-1i56</link>
      <guid>https://dev.to/aws-builders/serverless-iac-risk-analysis-the-architecture-behind-blast-radius-1i56</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: Introducing &lt;a href="https://github.com/sburgholzer/BlastRadius" rel="noopener noreferrer"&gt;Blast Radius&lt;/a&gt; — See What Breaks Before You Deploy&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first two articles in this series were about what Blast Radius does and how to wire it into your pipeline. This one is about how it's built. If you've ever wanted to design a system that analyzes infrastructure changes before they deploy, or you're just curious how the risk scores in your PR comment actually get calculated, this is the under-the-hood tour.&lt;/p&gt;

&lt;p&gt;I'm going to walk the same path the data walks: from the moment you submit a changeset, through format normalization, dependency discovery, scoring, the AI layer, and out to the frontend. Along the way I'll call out the decisions that mattered and a few that cost me a day to get right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four Constraints That Shaped Everything
&lt;/h2&gt;

&lt;p&gt;Every meaningful decision in Blast Radius traces back to one of four constraints. It's worth stating them up front, because the rest of this article is really just the consequences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to accept multiple IaC formats without the analysis engine knowing which one.&lt;/strong&gt; CDK, CloudFormation, and Terraform describe the same change three different ways. If the scoring logic had to branch on "is this Terraform or CloudFormation," every feature would multiply across every format, and adding a fourth tool would mean touching code everywhere. The engine has to see exactly one shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's asynchronous by nature.&lt;/strong&gt; A real analysis takes 10-30 seconds. It's making live calls to AWS Config and Resource Explorer, walking a dependency graph, and optionally waiting on a Bedrock model. You can't hold an API Gateway request open that long. The "submit" step and the "give me the answer" step have to be decoupled, which means state has to live somewhere in between.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It should cost nothing at idle and scale with concurrent PRs.&lt;/strong&gt; This is a tool that runs when someone opens an infrastructure PR. That might be zero times today and forty times tomorrow. Paying for an always-on cluster to serve bursty, unpredictable traffic is the wrong shape. Serverless bills per execution and scales out without anyone tuning autoscaling groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The output has to be explainable.&lt;/strong&gt; A gate that says "blocked: risk 82" and can't tell you why is a gate people rip out the first time it's wrong. Every number the system produces has to decompose back into the inputs that created it. That constraint is the reason the scoring formula is three weighted factors and not a trained model.&lt;/p&gt;

&lt;p&gt;Multi-format, async, serverless, explainable. Keep those in mind. Everything below is a response to one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Canonical Format — The One Decision That Matters Most
&lt;/h2&gt;

&lt;p&gt;If I had to point to a single choice that made the rest of the system tractable, it's this: normalize every input into one canonical format at the edge, then build exactly one analysis engine behind it.&lt;/p&gt;

&lt;p&gt;Here's the problem it solves. The same "replace this EC2 instance" event looks completely different depending on who's describing it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform encodes a replacement as an action array: &lt;code&gt;["delete", "create"]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CloudFormation encodes it as &lt;code&gt;Action: "Modify"&lt;/code&gt; with &lt;code&gt;Replacement: "True"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CDK calls it &lt;code&gt;changeType: "REPLACE"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three tools, one real-world event, three vocabularies. If discovery and scoring had to understand all three dialects, they'd carry that translation burden forever. Instead, an adapter translates each dialect into a single normalized record the moment it enters the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;resourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS::EC2::Instance&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;resourceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;i-abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;modificationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Replace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// one consistent representation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that point on, nothing downstream knows or cares where the change came from. Dependency discovery, risk scoring, visualization prep; they all operate on canonical manifests. Terraform-specific and CloudFormation-specific knowledge is quarantined inside the adapters.&lt;/p&gt;

&lt;p&gt;The payoff shows up when you add a new tool. Supporting Pulumi or Ansible isn't a cross-cutting change; it's one adapter. You write a function that turns the new tool's diff into canonical records, and the entire engine works unchanged. That's the first constraint satisfied structurally, not by discipline. The architecture makes the wrong thing hard to do.&lt;/p&gt;

&lt;p&gt;This is also why the pipeline decides whether to run an adapter as its very first branch, before anything else. The &lt;code&gt;sourceFormat&lt;/code&gt; field only exists on the original input; once the manifest is canonical, that information is intentionally gone. So the system asks "does this need conversion?" up front. If the input is already canonical, it skips the adapter entirely and goes straight to validation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyt9kepuovmblpgak6beq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyt9kepuovmblpgak6beq.png" alt="The canonical format funnel: three IaC formats converging through adapters into one manifest and one engine" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Three formats, three adapters, one canonical manifest, one engine.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Step Functions Orchestrates the Pipeline
&lt;/h2&gt;

&lt;p&gt;Given the async and serverless constraints, something has to coordinate a multi-step workflow where each step is a Lambda, steps can fail independently, and the whole thing runs unattended for half a minute. The candidates were the usual ones: chain Lambdas with SQS between them, fan events through EventBridge, or write a custom orchestrator.&lt;/p&gt;

&lt;p&gt;I chose Step Functions, and the reason is worth unpacking.&lt;/p&gt;

&lt;p&gt;SQS and EventBridge are great for decoupling events, but they're poor at expressing a workflow. The moment you need "run A, then B, then conditionally C, and if any of them fails, mark the whole thing failed," you end up implementing a state machine on top of queues: visibility timeouts, dead-letter queues, correlation IDs, and no single place to see where an execution actually is. A custom orchestrator has the same problem, plus you own all the retry and error semantics yourself.&lt;/p&gt;

&lt;p&gt;Step Functions gives you the workflow as a first-class, inspectable object. The full pipeline reads almost like the outline of the analysis itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is format "canonical"?
  No  → Adapter converts to canonical → prepare state
  Yes → skip adapter
→ Ingestion validates the manifest
→ Progress update (20%)
→ Resource Resolver discovers dependencies (AWS Config)
→ Progress update (40%)
→ Risk Assessor scores each resource
→ Progress update (60%)
→ Visualization Prep formats for the frontend + S3
→ Progress update (80%)
→ Is enableSummary = true?
  Yes → Risk Summary generates the AI explanation
  No  → skip summary
→ Mark analysis complete (100%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two implementation details in there are load-bearing, and both were the source of real bugs during development.&lt;/p&gt;

&lt;h3&gt;
  
  
  State preservation with &lt;code&gt;resultPath&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Each step needs the outputs of prior steps, not just its own immediate input. The naive approach — letting each Lambda's return value replace the state — throws away everything that came before. The pipeline uses &lt;code&gt;resultPath&lt;/code&gt; for Discovery, Scoring, and Visualization Prep, which nests each step's output under a new key instead of overwriting the state object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;After&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Ingestion:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;analysisId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;sourceFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;validatedManifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;After&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Discovery:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...above&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;discoveryResult:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dependencyGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;coverage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;After&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Scoring:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...above&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scoringResult:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;scoredResources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;riskSummary&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;After&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;VisPrep:&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...above&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;visualizationResult:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's why Scoring can be handed an explicit payload of &lt;code&gt;{ dependencyGraph: $.discoveryResult.dependencyGraph, manifest: $.validatedManifest }&lt;/code&gt; — the state still carries both. Using &lt;code&gt;outputPath&lt;/code&gt; here instead of &lt;code&gt;resultPath&lt;/code&gt; would silently drop the manifest, and the failure wouldn't surface until scoring tried to read a field that no longer existed.&lt;/p&gt;

&lt;h3&gt;
  
  
  No zombie analyses
&lt;/h3&gt;

&lt;p&gt;Every task state catches &lt;code&gt;States.ALL&lt;/code&gt;. On any failure, the pipeline routes to &lt;code&gt;UpdateStatusFailed&lt;/code&gt; (which marks the analysis &lt;code&gt;failed&lt;/code&gt; in DynamoDB) and then to a terminal &lt;code&gt;PipelineFailed&lt;/code&gt; state. This is a small thing that matters enormously for trust: it guarantees the frontend never shows an analysis stuck "running" forever. A crashed Lambda becomes a clean "failed" status the UI can render and the CLI can exit on, instead of a poll loop that never resolves. The CLI's stale detection (five unchanged polls = assumed failure) is a backstop, but the pipeline's catch-everything design is the primary guarantee.&lt;/p&gt;

&lt;p&gt;The conditional branches — adapter-or-not at the front, summary-or-not near the end — are the reason Step Functions fits. The summary check uses &lt;code&gt;Condition.and(isPresent('$.options.enableSummary'), booleanEquals(...))&lt;/code&gt; specifically so a missing field doesn't crash the choice state. Expressing that kind of branching in a queue-based system is possible but miserable. Here it's a &lt;code&gt;Choice&lt;/code&gt; state you can read at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dependency Discovery — Reading the Real Graph, Not the Code
&lt;/h2&gt;

&lt;p&gt;This is the part that makes Blast Radius more than a linter. Everything else operates on data; discovery goes and finds the data by asking AWS what's actually wired together in your account.&lt;/p&gt;

&lt;p&gt;The Resource Resolver is the most complex Lambda in the system, and the algorithm is a bounded graph traversal. For each changed resource it queries AWS Config for relationships, then recursively walks those relationships up to &lt;code&gt;maxDepth&lt;/code&gt; (default 5), building a graph of nodes and edges as it goes. The Config query itself is a SQL-like statement against the configuration aggregator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resourceType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;awsRegion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;resourceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;resourceId&amp;gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight is why AWS Config. It tracks the live relationship graph: which security group is attached to which network interface, which instance sits behind which load balancer. That's the ground truth IaC tools are blind to. Resource Explorer is the fallback for resources Config doesn't have full relationship data on, and that feeds a coverage classification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full&lt;/strong&gt; — Config returned relationship data for this resource.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial&lt;/strong&gt; — Resource Explorer found the resource but Config had limited data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unknown&lt;/strong&gt; — the resource couldn't be found in either service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Coverage is surfaced honestly rather than hidden. If the graph is incomplete, the output says so.&lt;/p&gt;

&lt;p&gt;Three engineering details keep discovery correct and affordable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cycle protection.&lt;/strong&gt; Real infrastructure graphs have cycles. The traversal carries a visited-set so a circular relationship (A depends on B depends on A) terminates instead of looping forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An LRU cache, 10,000 entries.&lt;/strong&gt; Deep graphs re-encounter the same resources constantly. Caching Config responses turns a query storm into something far cheaper, and the resolver reports &lt;code&gt;cacheStats: { hits, misses }&lt;/code&gt; so the effect is measurable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filtering incomplete relationships at the source.&lt;/strong&gt; Config sometimes returns relationship entries with a missing &lt;code&gt;resourceId&lt;/code&gt;. If those slipped through, they'd become orphan nodes and dangling edges — a graph that renders wrong and scores wronger. They're dropped the moment they come back from Config, which is the cleanest place to enforce the invariant. (The API layer filters again before handing data to the frontend, as defense in depth.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The resolver runs with 1024 MB of memory and a 90-second timeout. Config result sets can be large, and this is the one step where being generous with resources pays off. It also uses a retry-with-backoff wrapper for its AWS calls: transient throttling retries (three attempts), and a service failure degrades the resource's coverage rather than aborting the whole analysis. A Config error alone drops the resource to &lt;code&gt;partial&lt;/code&gt;; both Config and Resource Explorer failing drops to &lt;code&gt;unknown&lt;/code&gt;. Either way the pipeline continues instead of dying over one unresolvable resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Risk Formula — Deliberately Simple
&lt;/h2&gt;

&lt;p&gt;The explainability constraint is most visible here. It would have been easy, and worse, to train a model. Instead, every resource's risk is a weighted sum of three factors, each normalized to 0-100:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;impactScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;depthScore&lt;/span&gt;        &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;criticalityScore&lt;/span&gt;  &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changeTypeSeverity&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three factors answer three intuitive questions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Question it answers&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Depth&lt;/td&gt;
&lt;td&gt;How far from the change?&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;Depth 1 = 100, depth 5 = 60, depth 10 = 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Criticality&lt;/td&gt;
&lt;td&gt;How important is this resource type?&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;Database = 100, Lambda = 75, S3 = 50, log group = 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change severity&lt;/td&gt;
&lt;td&gt;How dangerous is the action?&lt;/td&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;Remove = 100, Replace = 80, Modify = 50, Add = 30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Criticality carries the most weight, and that's on purpose. What is affected matters more than how close it is or what's being done to it. Deleting a throwaway log group one hop away is genuinely less scary than modifying something three hops from a production database, and the weights are tuned so the numbers agree with that intuition. A Critical database at depth 1 hit by a Remove scores 100 (maximum danger); a Low log group at depth 5 hit by a Modify lands around 35 (medium, probably fine). Depth and change type split the remaining 60% evenly because both matter and neither should dominate.&lt;/p&gt;

&lt;p&gt;The score-to-category mapping is a plain linear cut, and that's intentional too:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;75-100&lt;/td&gt;
&lt;td&gt;Critical (red)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-74&lt;/td&gt;
&lt;td&gt;High (orange)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-49&lt;/td&gt;
&lt;td&gt;Medium (yellow)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-24&lt;/td&gt;
&lt;td&gt;Low (green)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No curves, no thresholds that shift based on the graph. A reviewer can look at any score, ask "which factor drove that," and get a straight answer. Each scored resource even carries its &lt;code&gt;dependencyChain&lt;/code&gt; — the ordered breadcrumb like &lt;code&gt;sg-abc123 → ec2-instance-1 → rds-prod&lt;/code&gt; — and its &lt;code&gt;highestRiskPath&lt;/code&gt;, the actual worst-case edges. So "why is this an 82" is always answerable from the resource itself. That auditability is the whole point. A gate people trust is a gate they can interrogate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI Layer — Advisory, Structured, and Optional
&lt;/h2&gt;

&lt;p&gt;The scoring formula is good at "how bad is each individual resource." It's deliberately bad at "is there a systemic pattern here." A fan-out where ten resources each score a harmless 65 but all hang off the single security group you're touching. Any one is fine; all ten failing together is an outage. That's the gap the AI layer fills.&lt;/p&gt;

&lt;p&gt;Three principles govern how it's wired in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI reasons over the highest-risk resources; it doesn't compute the scores.&lt;/strong&gt; There's a clean division of labor. The deterministic engine produces the scored resources and the dependency graph. The Risk Summary Lambda selects the top few highest-scoring resources, hands them plus the overall risk summary to a Bedrock model (Anthropic Claude by default; the model ID is configurable via &lt;code&gt;BEDROCK_MODEL_ID&lt;/code&gt;), and asks for a judgment about the deployment as a whole. The model isn't second-guessing arithmetic. It's reasoning about structure the arithmetic can't see. The whole call runs under a 15-second timeout and degrades gracefully: if Bedrock times out or errors, the pipeline still completes with scores intact, just without the narrative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its output is structured, not prose-only.&lt;/strong&gt; The Lambda parses the model's response into a typed shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"…markdown…"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"recommendDeploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is what makes the &lt;code&gt;--ai-gate&lt;/code&gt; from the previous article possible. A CI gate can't act on a paragraph of English; it can act on &lt;code&gt;recommendDeploy: false&lt;/code&gt;. The natural-language &lt;code&gt;summary&lt;/code&gt; gets written back into the S3 &lt;code&gt;visualization.json&lt;/code&gt; so the frontend and PR comment can render it, but the decision is a boolean the pipeline can branch on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's optional at every level.&lt;/strong&gt; The summary step is a conditional branch in the state machine, gated on &lt;code&gt;enableSummary&lt;/code&gt;. The Lambda itself checks both &lt;code&gt;ENABLE_BEDROCK_SUMMARY&lt;/code&gt; and &lt;code&gt;ENABLE_BEDROCK&lt;/code&gt; before doing anything. If Bedrock is disabled server-side, threshold gates keep working perfectly and the AI gate returns a clean error rather than a confusing empty result. This matters because Bedrock model access isn't universal. The system has to be fully useful without it, and it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend and API — Where the Shapes Meet
&lt;/h2&gt;

&lt;p&gt;Two design decisions on the presentation side are worth surfacing, because they're the kind of thing that's invisible when it works and infuriating when it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime config, not build-time config.&lt;/strong&gt; The frontend is a static React SPA served from CloudFront, but it needs to know the API Gateway URL, which isn't known until the stack deploys. Baking the URL in at build time would mean rebuilding the frontend for every deployment and every environment. Instead, CDK writes a &lt;code&gt;/config.json&lt;/code&gt; alongside the static assets, and the SPA fetches it at runtime to resolve the API URL. The same build artifact works across environments and in both auth and no-auth modes. It's a small pattern that removes a whole category of "works in staging, wrong URL in prod" bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The API translates between two vocabularies.&lt;/strong&gt; The pipeline's internal visualization format speaks in &lt;code&gt;nodes&lt;/code&gt; and &lt;code&gt;edges&lt;/code&gt;. The frontend wants &lt;code&gt;scoredResources&lt;/code&gt; and a &lt;code&gt;dependencyGraph&lt;/code&gt; shaped for Cytoscape.js. Rather than push that translation into the React app, where it'd be re-implemented and drift, the API handler does the mapping in one place. It also does the final orphan-node and dangling-edge filtering here, so the frontend is guaranteed a clean, renderable graph and never has to defend against malformed input. Put the adaptation at the boundary, keep the consumer simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;A few things this system taught me that generalize well beyond Blast Radius.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js 22 Lambda handlers must be async.&lt;/strong&gt; This one cost real time. On the Node.js 22 runtime, a synchronous handler returns &lt;code&gt;null&lt;/code&gt; — the runtime resolves before your work does. The adapters were the victims: written as plain synchronous functions, they'd "succeed" and hand the pipeline a null manifest, and the failure surfaced three steps later in discovery where it made no sense. The fix is trivial (make handlers &lt;code&gt;async&lt;/code&gt;); finding it was not. If you're on a recent Node Lambda runtime, make every handler async by default and save yourself the afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency injection beats module mocking for testability.&lt;/strong&gt; Every Lambda that talks to AWS takes an optional &lt;code&gt;deps&lt;/code&gt; parameter. In tests you pass mock clients directly; in production the Lambda builds its own. The subtle part is how you detect which you got, because the Lambda runtime passes the &lt;code&gt;Context&lt;/code&gt; object as the second argument, which is truthy. A naive &lt;code&gt;deps ?? createDefaultDeps()&lt;/code&gt; would treat the Context as your dependencies and explode. The pattern that actually works is a shape check: &lt;code&gt;deps &amp;amp;&amp;amp; 'configClient' in deps ? deps : createDefaultDeps()&lt;/code&gt;. It looks fussy, but it's the difference between tests that inject cleanly and a production crash that only reproduces in the real runtime. (The next article goes deep on the testing philosophy this enables: property-based tests, and zero &lt;code&gt;vi.mock()&lt;/code&gt; calls.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long-running pipelines need progress, not just a spinner.&lt;/strong&gt; A 30-second analysis with no feedback feels broken. The pipeline emits explicit progress updates at fixed milestones — 20% after ingestion, 40% after discovery, 60% after scoring, 80% after visualization, 100% at completion — written to DynamoDB where the frontend and CLI can poll them. The percentages are coarse on purpose. They map to real pipeline stages rather than a fake animation, so "stuck at 40%" is actually diagnostic information (discovery is slow or wedged) rather than decoration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Every one of those decisions maps back to a constraint. The canonical format is how Blast Radius supports multiple IaC tools without the engine ever knowing which one produced the change. Step Functions is how a 30-second workflow runs asynchronously without a zombie analysis in sight. Serverless primitives are how it costs nothing when nobody's opening a PR. And the transparent three-factor formula is how a risk score stays something you can argue with instead of something you have to take on faith.&lt;/p&gt;

&lt;p&gt;Next in this series, we get into the build itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article 4: 349 Tests, Zero Mocks — Building Blast Radius in TypeScript:&lt;/strong&gt; the engineering story. Property-based testing with fast-check, dependency injection over module mocking, the adapter pattern in practice, and an honest retrospective on what I'd do differently.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>cicd</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Stop Deploying Blind: Add a Blast Radius Gate to Your CI/CD Pipeline</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Thu, 09 Jul 2026 03:10:20 +0000</pubDate>
      <link>https://dev.to/aws-builders/stop-deploying-blind-add-a-blast-radius-gate-to-your-cicd-pipeline-231m</link>
      <guid>https://dev.to/aws-builders/stop-deploying-blind-add-a-blast-radius-gate-to-your-cicd-pipeline-231m</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: Introducing &lt;a href="https://github.com/sburgholzer/BlastRadius" rel="noopener noreferrer"&gt;Blast Radius&lt;/a&gt; — See What Breaks Before You Deploy&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;This article shows you how to use Blast Radius in your own CI/CD pipeline. By the end, you'll have a deployment gate that automatically analyzes infrastructure changes, scores downstream risk, and blocks merges that exceed your threshold.&lt;/p&gt;

&lt;p&gt;Blast Radius comments on your PR with an analysis table (Highest Score, Affected Resources, AI Recommendation, Confidence) plus an AI-generated summary.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gs37tnxk6c7v7tw3oov.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9gs37tnxk6c7v7tw3oov.png" alt="Example of a Blast Radius PR Comment" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are currently two paths to use Blast Radius:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Action:&lt;/strong&gt; fastest and auto-comments on PRs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw CLI:&lt;/strong&gt; works in any CI system such as GitLab, Jenkins, CircleCI, or whatever you're running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Blast Radius is meant for your infrastructure. It works on any stage: dev, testing, production, or any stages you may use. It calls live AWS APIs (Config and Resource Explorer) against your actual account. The results reflect real dependency relationships, not static analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites — Deploy the Backend
&lt;/h2&gt;

&lt;p&gt;The following must be enabled in your AWS Account before deploying the backend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Config&lt;/strong&gt; must be enabled and recording in the region you deploy to. This is where Blast Radius reads dependency relationships. &lt;a href="https://docs.aws.amazon.com/config/latest/developerguide/gs-console.html" rel="noopener noreferrer"&gt;Here's how to enable AWS Config.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Explorer&lt;/strong&gt; must be enabled. It's the fallback for resources Config doesn't have relationships for. &lt;a href="https://docs.aws.amazon.com/resource-explorer/latest/userguide/getting-started-setting-up.html" rel="noopener noreferrer"&gt;Here's how to enable Resource Explorer.&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CDK Bootstrap&lt;/strong&gt; must be run via &lt;code&gt;cdk bootstrap&lt;/code&gt; in your account and region you are deploying to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional: Bedrock model access&lt;/strong&gt; if you want AI summaries. You may choose any model via cross-region inference profile. Without enabling Bedrock model access, threshold gates will still work but the AI gate will not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy Steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sburgholzer/BlastRadius.git
&lt;span class="nb"&gt;cd &lt;/span&gt;BlastRadius
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build
&lt;span class="nb"&gt;cd &lt;/span&gt;packages/infra
cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates an API Gateway, Step Functions state machine, Lambda functions, DynamoDB tables, S3 bucket, and a CloudFront distribution for the frontend.&lt;/p&gt;

&lt;p&gt;When the deployment finishes, grab the API URL from the CDK output. That's the only value you need for the rest of this article.&lt;/p&gt;

&lt;p&gt;To enable AI summaries, set &lt;code&gt;enableBedrockSummary&lt;/code&gt; in the CDK props. You can also configure &lt;code&gt;resultsRetentionDays&lt;/code&gt; for S3 lifecycle management. More detail on these options is in the README.&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub Action — The Five-Minute Path
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Terraform Workflow
&lt;/h3&gt;

&lt;p&gt;Terraform is the simplest path. Generate the plan, convert it to JSON, and pass it to the action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform plan -out=plan.out&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform show -json plan.out &amp;gt; plan.json&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sburgholzer/BlastRadius@v0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-plan&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;
    &lt;span class="na"&gt;ai-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;api-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BLAST_RADIUS_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The action reads the JSON plan, runs the analysis, and comments the results on your PR.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDK Workflow
&lt;/h3&gt;

&lt;p&gt;CDK requires a few more steps. Blast Radius needs to know what CloudFormation will actually do — the changeset actions, not just what the template looks like. A &lt;code&gt;cdk synth&lt;/code&gt; alone doesn't give you that; you have to ask CloudFormation directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate CloudFormation changeset&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;npx cdk synth 2&amp;gt;/dev/null&lt;/span&gt;
    &lt;span class="s"&gt;cp cdk.out/MyStack.template.json template.json&lt;/span&gt;
    &lt;span class="s"&gt;CHANGESET_NAME="blast-radius-$(date +%s)"&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation create-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--template-body file://template.json \&lt;/span&gt;
      &lt;span class="s"&gt;--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-type UPDATE \&lt;/span&gt;
      &lt;span class="s"&gt;--output json&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation wait change-set-create-complete \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation describe-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--output json &amp;gt; changeset.json&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation delete-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blast Radius Analysis&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sburgholzer/BlastRadius@v0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blast-radius&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudformation&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changeset.json&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;
    &lt;span class="na"&gt;ai-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;api-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BLAST_RADIUS_API_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step has a specific job: synth produces the template, create-change-set asks CloudFormation what it would change, wait holds until it's ready, describe-change-set captures the answer as JSON, and delete-change-set cleans up so it doesn't linger between runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudFormation Workflow
&lt;/h3&gt;

&lt;p&gt;Same as CDK but without the synth step — you already have a template file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate CloudFormation changeset&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;CHANGESET_NAME="blast-radius-$(date +%s)"&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation create-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--template-body file://template.json \&lt;/span&gt;
      &lt;span class="s"&gt;--capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-type UPDATE \&lt;/span&gt;
      &lt;span class="s"&gt;--output json&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation wait change-set-create-complete \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation describe-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME \&lt;/span&gt;
      &lt;span class="s"&gt;--output json &amp;gt; changeset.json&lt;/span&gt;
    &lt;span class="s"&gt;aws cloudformation delete-change-set \&lt;/span&gt;
      &lt;span class="s"&gt;--stack-name MyStack \&lt;/span&gt;
      &lt;span class="s"&gt;--change-set-name $CHANGESET_NAME&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blast Radius Analysis&lt;/span&gt;
  &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sburgholzer/BlastRadius@v0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cloudformation&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;changeset.json&lt;/span&gt;
    &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;75&lt;/span&gt;
    &lt;span class="na"&gt;ai-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;api-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BLAST_RADIUS_API_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is essentially the same as the CDK Workflow, except we don't need to run a synth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the GitHub Action Does Under the Hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Downloads the bundled CLI from the GitHub release&lt;/li&gt;
&lt;li&gt;Runs &lt;code&gt;analyze&lt;/code&gt; with &lt;code&gt;--ci&lt;/code&gt; flag for JSON output&lt;/li&gt;
&lt;li&gt;Parses the JSON into GitHub Action outputs&lt;/li&gt;
&lt;li&gt;Comments on the PR with a formatted table + AI summary (disable with &lt;code&gt;comment-pr: false&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Action inputs:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Required&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;format&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Input format: cloudformation / terraform-plan / canonical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;input&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Path to the input file (changeset JSON / terraform plan JSON / canonical manifest)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;api-url&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Blast Radius API URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;threshold&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Risk score threshold (0-100). Fails if any resource exceeds this.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ai-gate&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Fail if AI recommends against deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no-summary&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;Skip AI summary generation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;comment-pr&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;Automatically comment results on the PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;version&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;latest&lt;/td&gt;
&lt;td&gt;CLI version to use&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Action outputs:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;analysis-id&lt;/td&gt;
&lt;td&gt;The analysis ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;verdict&lt;/td&gt;
&lt;td&gt;pass or fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;highest-score&lt;/td&gt;
&lt;td&gt;Highest impact score (0-100)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;total-affected&lt;/td&gt;
&lt;td&gt;Total affected resources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recommend-deploy&lt;/td&gt;
&lt;td&gt;AI deployment recommendation (true/false)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;confidence&lt;/td&gt;
&lt;td&gt;AI recommendation confidence (high/medium/low)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;summary&lt;/td&gt;
&lt;td&gt;AI-generated risk summary (markdown)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;result-json&lt;/td&gt;
&lt;td&gt;Full JSON result (for custom downstream steps)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  CLI in Any CI System
&lt;/h2&gt;

&lt;p&gt;The GitHub Action is a convenience wrapper. Underneath, it's the CLI. If you're on GitLab, Jenkins, CircleCI, Bitbucket Pipelines, any other tool, or want greater control than the GitHub Action, you can use the CLI directly.&lt;/p&gt;

&lt;p&gt;It's a single bundled JavaScript file. No npm install, no dependencies beyond a Node.js runtime. Download it, run it, read the exit code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How to get the CLI:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://github.com/sburgholzer/BlastRadius/releases/latest/download/blast-radius.js &lt;span class="nt"&gt;-o&lt;/span&gt; blast-radius.js
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'1{/^#!/d}'&lt;/span&gt; blast-radius.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sed&lt;/code&gt; command strips the shebang line. Node.js can't parse &lt;code&gt;#!/usr/bin/env node&lt;/code&gt; when you invoke it with &lt;code&gt;node blast-radius.js&lt;/code&gt; directly (needed in CI environments that don't support executing .js files as scripts).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment setup:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;BLAST_RADIUS_API_URL&lt;/code&gt; as an environment variable pointing to your deployed API Gateway URL. That's the only required config. Everything else goes in command flags.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab CI example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;blast-radius&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;validate&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sL https://github.com/sburgholzer/BlastRadius/releases/latest/download/blast-radius.js -o blast-radius.js&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sed -i '1{/^#!/d}' blast-radius.js&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node blast-radius.js analyze --format terraform-plan --input plan.json --threshold 75 --ai-gate --ci&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;BLAST_RADIUS_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$BLAST_RADIUS_URL&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;blast-radius-result.json&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: &lt;code&gt;--ci&lt;/code&gt; outputs JSON to stdout. Capture it with &lt;code&gt;&amp;gt; blast-radius-result.json&lt;/code&gt; and save as an artifact for downstream jobs. Exit code 1 naturally fails the GitLab job, no extra configuration needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Jenkins example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Blast Radius'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;environment&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BLAST_RADIUS_API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'blast-radius-url'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'''
      curl -sL https://github.com/sburgholzer/BlastRadius/releases/latest/download/blast-radius.js -o blast-radius.js
      sed -i '1{/^#!/d}' blast-radius.js
      node blast-radius.js analyze --format cloudformation --input changeset.json --threshold 75 --ai-gate --ci
    '''&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API URL in Jenkins credentials and reference it as an environment variable. Non-zero exit fails the stage, standard Jenkins behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  CircleCI example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blast Radius Analysis&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;curl -sL https://github.com/sburgholzer/BlastRadius/releases/latest/download/blast-radius.js -o blast-radius.js&lt;/span&gt;
      &lt;span class="s"&gt;sed -i '1{/^#!/d}' blast-radius.js&lt;/span&gt;
      &lt;span class="s"&gt;node blast-radius.js analyze --format terraform-plan --input plan.json --ai-gate --ci&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BLAST_RADIUS_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${BLAST_RADIUS_URL}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Parsing the JSON output
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;--ci&lt;/code&gt;, the CLI outputs a JSON object to stdout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"analysisId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exitCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ai-gate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recommendDeploy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"riskSummary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"highestScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalAffected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"critical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"medium"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"low"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"naturalLanguageSummary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"## Executive Overview&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;This deployment presents..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What you can do with this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pipe to jq for specific fields: &lt;code&gt;node blast-radius.js analyze ... --ci | jq '.riskSummary.highestScore'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Send to Slack: extract &lt;code&gt;verdict&lt;/code&gt;, &lt;code&gt;highestScore&lt;/code&gt;, and &lt;code&gt;naturalLanguageSummary&lt;/code&gt; into a webhook payload.&lt;/li&gt;
&lt;li&gt;Build a custom PR comment in GitLab/Bitbucket using the JSON fields.&lt;/li&gt;
&lt;li&gt;Feed into a dashboard or metrics system; track risk scores over time per team or per service.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exit Codes
&lt;/h2&gt;

&lt;p&gt;Any CI system's native "fail on non-zero" behavior works out of the box. No special error handling needed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Pass - threshold OK and AI approves (or gates not enabled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Fail - threshold exceeded OR AI recommends against deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Error - invalid input / pipeline failure / timeout / misconfiguration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Choosing Your Gate Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Threshold only (&lt;code&gt;--threshold N&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is deterministic. Same input, same result, every time. It's based on the scoring formula from the previous article: depth (how far the resource is from the changed resource), criticality (how critical the resource is to your infrastructure), and change severity (how much the change affects the resource). Each factor is weighted to produce a score between 0 and 100.&lt;/p&gt;

&lt;p&gt;Because it's a formula, there's no AI dependency. If you run Blast Radius without Bedrock, you still get a usable result for your CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;This gate is good for compliance-driven teams, environments where you need reproducible gates, or if you want to understand how the scoring system works before enabling AI.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;N&lt;/code&gt; is your cutoff: if the highest score of any resource exceeds that value, Blast Radius fails your pipeline. Here's a general guideline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;90:&lt;/strong&gt; very permissive. Only blocks Critical-category changes. Good for dev/staging environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;75:&lt;/strong&gt; moderate. Blocks Critical and most High-category hits. Good default for production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50:&lt;/strong&gt; conservative. Blocks anything above Medium. Good for sensitive workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start at 75 to see how it works in your environment, then fine-tune from there.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI gate only (&lt;code&gt;--ai-gate&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is where judgment-based thinking comes into play. The AI gate looks at the full graph, not just the individual scores. It catches patterns like shared dependencies, fan-out risk, and cascading failure paths. It will return &lt;code&gt;recommendDeploy: true/false&lt;/code&gt; with &lt;code&gt;confidence: high/medium/low&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is good for teams that trust AI tooling or want to experiment with it to catch systemic risks. If you use this gate, I recommend reading through the summary and doing manual investigation to verify the reasoning, and to understand what to fix when it blocks.&lt;/p&gt;

&lt;p&gt;One caveat: &lt;code&gt;--ai-gate&lt;/code&gt; requires Bedrock enabled on the backend. If Bedrock is disabled, the CLI exits with error code 2.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Both Gates Together
&lt;/h3&gt;

&lt;p&gt;This is my recommended configuration. When you use both gates, Blast Radius fails your pipeline if EITHER triggers. The threshold catches obvious high-scoring resources. The AI catches non-obvious patterns below your threshold value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Without AI Summary
&lt;/h3&gt;

&lt;p&gt;If you run Blast Radius with &lt;code&gt;--no-summary&lt;/code&gt;, it skips the AI-generated natural language summary but will still run the analysis. This saves a few seconds if you only care about the score.&lt;/p&gt;

&lt;p&gt;Note: &lt;code&gt;--ai-gate&lt;/code&gt; overrides &lt;code&gt;--no-summary&lt;/code&gt; because the gate needs the AI to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the Output
&lt;/h2&gt;

&lt;p&gt;You've set up the gate. It ran. Now you're looking at a PR comment (or JSON output) with scores, dependency chains, and an AI recommendation. Here's how to read it all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PR Comment Anatomy
&lt;/h3&gt;

&lt;p&gt;The GitHub Action comments on your PR with two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Summary table:&lt;/strong&gt; four key metrics at a glance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Highest Score: the single scariest number in the analysis (0-100)&lt;/li&gt;
&lt;li&gt;Affected Resources: total number of downstream resources discovered&lt;/li&gt;
&lt;li&gt;AI Recommendation: Deploy ✅ or Do Not Deploy ❌&lt;/li&gt;
&lt;li&gt;Confidence: how sure the AI is (high/medium/low)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;AI-generated summary&lt;/strong&gt; (if Bedrock is enabled): structured markdown with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Executive overview: one-paragraph assessment&lt;/li&gt;
&lt;li&gt;Key findings: the specific resources and patterns that drove the recommendation&lt;/li&gt;
&lt;li&gt;Cascading risks: what could go wrong if you deploy anyway&lt;/li&gt;
&lt;li&gt;Recommendation: the final call with reasoning&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're using the CLI with &lt;code&gt;--ci&lt;/code&gt;, you get the same information as JSON fields: &lt;code&gt;riskSummary&lt;/code&gt;, &lt;code&gt;recommendDeploy&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, and &lt;code&gt;naturalLanguageSummary&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk Scores — What the Numbers Mean
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;75-100&lt;/td&gt;
&lt;td&gt;Critical (red)&lt;/td&gt;
&lt;td&gt;Direct or near-direct impact on high-criticality resources (databases, auth systems, core networking). Stop and think.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-74&lt;/td&gt;
&lt;td&gt;High (orange)&lt;/td&gt;
&lt;td&gt;Significant downstream impact. Review the dependency chain before deploying.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-49&lt;/td&gt;
&lt;td&gt;Medium (yellow)&lt;/td&gt;
&lt;td&gt;Worth noting. Probably fine, but look at what's at the end of the chain.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0-24&lt;/td&gt;
&lt;td&gt;Low (green)&lt;/td&gt;
&lt;td&gt;Far away, low criticality, or a non-destructive change type. Safe to proceed.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  How the Score is Calculated (review)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;impactScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;depthScore&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;criticalityScore&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;changeTypeSeverity&lt;/span&gt; &lt;span class="err"&gt;×&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Depth (30%)&lt;/strong&gt; — how many hops away. Depth 1 = 100, Depth 5 = 60, Depth 10 = 10.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Criticality (40%)&lt;/strong&gt; — how important the resource type is. Database = 100, Lambda = 75, S3 = 50, Log group = 25.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Change severity (30%)&lt;/strong&gt; — how dangerous the action is. Remove = 100, Replace = 80, Modify = 50, Add = 30.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Criticality is weighted highest because what's affected matters more than how far away it is. Deleting a test log group one hop away is less dangerous than modifying something three hops from a production database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependency Chains — the Breadcrumb Trail
&lt;/h3&gt;

&lt;p&gt;Every scored resource includes a &lt;code&gt;dependencyChain&lt;/code&gt;: the ordered path showing exactly how risk flows from your change to that resource.&lt;/p&gt;

&lt;p&gt;Example: &lt;code&gt;sg-abc123 → ec2-instance-1 → rds-prod&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Read left to right: your changed resource → intermediate resources → the thing at risk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Longer chains mean lower depth scores (the resource is further away).&lt;/li&gt;
&lt;li&gt;But a long chain ending at a Critical-class resource (like a database) can still score high because criticality carries 40% of the weight.&lt;/li&gt;
&lt;li&gt;Short chains ending at low-criticality resources score low even though they're close.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the frontend graph view, the dependency chain is the highlighted path when you click a node. In the PR comment, it appears in the detail expansion for each resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Confidence Levels
&lt;/h3&gt;

&lt;p&gt;When using the AI gate, the model returns a confidence alongside its recommendation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High:&lt;/strong&gt; clear signal. The graph strongly supports the recommendation. Trust it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Medium:&lt;/strong&gt; mixed signals. Some risk factors present, some mitigating. Worth human review before overriding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low:&lt;/strong&gt; AI isn't sure. Limited data, ambiguous patterns, or a very small graph. Don't gate on this alone — treat it as advisory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When the AI says "don't deploy" but scores are below threshold:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the AI gate earning its keep. It's seeing a pattern that individual scores don't capture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fan-out risk: many resources all depending on one thing you're changing.&lt;/li&gt;
&lt;li&gt;Single points of failure: a resource that's the only path between your change and critical infrastructure.&lt;/li&gt;
&lt;li&gt;Cascading depth: the graph is deep and converges on something important at the far end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check the AI summary for the specific reasoning. It will explain what pattern it detected and why it recommends against deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When the AI says "deploy" but you're nervous:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The AI can be wrong. If your gut says something's off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look at the dependency chain for the highest-scored resource. Does the path make sense?&lt;/li&gt;
&lt;li&gt;Check whether the graph is missing resources (AWS Config relationships aren't always complete).&lt;/li&gt;
&lt;li&gt;Use the interactive frontend to explore the full graph visually.&lt;/li&gt;
&lt;li&gt;When in doubt, deploy to a non-production stage first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local Development and Testing
&lt;/h2&gt;

&lt;p&gt;You don't have to go straight to CI/CD pipeline. The CLI has commands for generating, inspecting, and exploring results locally. The interactive frontend also gives you a visual way to understand your infrastructure's dependency landscape.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;blast-radius generate&lt;/code&gt; — Create Input Files without Submitting
&lt;/h3&gt;

&lt;p&gt;The generate command produces the same input file that &lt;code&gt;analyze&lt;/code&gt; would create and submit, but stops there. No analysis is run, no API is called.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CDK: synthesizes and creates a changeset, saves to disk&lt;/span&gt;
blast-radius generate &lt;span class="nt"&gt;--format&lt;/span&gt; cdk &lt;span class="nt"&gt;--stack&lt;/span&gt; MyStack &lt;span class="nt"&gt;--output&lt;/span&gt; changeset.json

&lt;span class="c"&gt;# Terraform: runs terraform plan + show, saves the JSON&lt;/span&gt;
blast-radius generate &lt;span class="nt"&gt;--format&lt;/span&gt; terraform-plan &lt;span class="nt"&gt;--output&lt;/span&gt; plan.json

&lt;span class="c"&gt;# CloudFormation: creates changeset from template, saves it&lt;/span&gt;
blast-radius generate &lt;span class="nt"&gt;--format&lt;/span&gt; cloudformation &lt;span class="nt"&gt;--stack&lt;/span&gt; MyStack &lt;span class="nt"&gt;--template&lt;/span&gt; cfn.json &lt;span class="nt"&gt;--output&lt;/span&gt; changeset.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why this is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify your CI workflow produces valid input before enabling the gate&lt;/li&gt;
&lt;li&gt;Inspect exactly what Blast Radius will see — review the changeset JSON to understand the resources and change types&lt;/li&gt;
&lt;li&gt;Build a library of test inputs for different scenarios&lt;/li&gt;
&lt;li&gt;Share with teammates: "here's what the analysis would run against"&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--save&lt;/code&gt; flag — save AND submit
&lt;/h3&gt;

&lt;p&gt;If you want to both inspect the input and run the analysis in one shot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;blast-radius analyze &lt;span class="nt"&gt;--format&lt;/span&gt; cdk &lt;span class="nt"&gt;--stack&lt;/span&gt; MyStack &lt;span class="nt"&gt;--save&lt;/span&gt; changeset.json &lt;span class="nt"&gt;--ai-gate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This saves the generated input to disk, then submits it. When a result comes back surprising ("why did my security group change score 82?"), you can open &lt;code&gt;changeset.json&lt;/code&gt; and see exactly what was sent: which resources, which change types, which properties.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Interactive Frontend
&lt;/h3&gt;

&lt;p&gt;After deploying the backend, the CloudFront URL serves a React SPA where you can explore results visually.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Analysis list&lt;/strong&gt; (&lt;code&gt;/analyses&lt;/code&gt;): shows all past analyses sorted by date. Click any row to drill in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency graph:&lt;/strong&gt; Cytoscape.js interactive graph. Your changed resources appear as solid blue nodes. Downstream dependencies fan out, colored by risk category (red/orange/yellow/green). Click any node for a detail panel: resource ID, type, score, risk category, and full dependency chain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters:&lt;/strong&gt; filter by risk category, resource type, or toggle direct changes on/off. Filters apply as intersection (resource must match all active filters).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Table view:&lt;/strong&gt; sortable alternative to the graph. Sorted by impact score (highest first) by default. Paginated at 50 rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export:&lt;/strong&gt; download the full analysis as JSON for archiving or feeding into other tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Summary:&lt;/strong&gt; rendered as formatted markdown below the graph (if Bedrock was enabled for the analysis).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend is useful for exploring results visually after CI runs, understanding dependency patterns in your account before they cause problems, demoing Blast Radius to stakeholders, and investigating why the AI gate blocked a deployment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running against the demo examples
&lt;/h3&gt;

&lt;p&gt;The repo includes &lt;code&gt;examples/cdk-demo/&lt;/code&gt; with a two-step demo that doesn't require your real production infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;01-baseline/&lt;/code&gt;: deploys a realistic infrastructure: VPC, ECS Fargate service, Aurora PostgreSQL cluster, ALB, Lambda function, S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;02-risky-change/&lt;/code&gt;: modifies the security group (restricts from public to internal-only) and resizes the database instance.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy baseline&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;examples/cdk-demo/01-baseline
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npx cdk deploy

&lt;span class="c"&gt;# Run Blast Radius against the risky change&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../02-risky-change
npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;BLAST_RADIUS_API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-url&amp;gt; blast-radius analyze &lt;span class="nt"&gt;--format&lt;/span&gt; cdk &lt;span class="nt"&gt;--stack&lt;/span&gt; BlastRadiusDemoBaseline &lt;span class="nt"&gt;--ai-gate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces a real analysis showing 7 affected resources cascading from the security group and database changes, ECS service, Lambda, ingress/egress rules, scored and visualized.&lt;/p&gt;

&lt;p&gt;Cost note: The demo creates real AWS resources (~$0.45/hr). Run &lt;code&gt;cdk destroy&lt;/code&gt; when you're done. Don't do what I did and forget to run the destroy!&lt;/p&gt;

&lt;h3&gt;
  
  
  Other useful commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check status of a running analysis&lt;/span&gt;
blast-radius status &lt;span class="nt"&gt;--analysis-id&lt;/span&gt; abc-123

&lt;span class="c"&gt;# Fetch completed results&lt;/span&gt;
blast-radius &lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nt"&gt;--analysis-id&lt;/span&gt; abc-123 &lt;span class="nt"&gt;--format&lt;/span&gt; json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;You now have a working deployment gate. Every infrastructure PR gets analyzed for downstream risk before it can merge. Whether you're using the GitHub Action, the CLI in GitLab, or exploring results in the frontend, the workflow is the same: generate, analyze, gate, decide.&lt;/p&gt;

&lt;p&gt;Next in this series, we go under the hood:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Article 3: Serverless IaC Risk Analysis — The Architecture Behind Blast Radius:&lt;/strong&gt; How the pipeline is designed. Why Step Functions. How the canonical format abstraction makes multi-IaC support possible without touching the analysis engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article 4: 349 Tests, Zero Mocks — Building Blast Radius in TypeScript:&lt;/strong&gt; Engineering decisions, property-based testing with fast-check, dependency injection over module mocking, and the Lambda runtime gotchas that wasted a day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These articles are coming in the next few weeks. I'll update this post with links as each one publishes.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>cicd</category>
      <category>infrastructureascode</category>
    </item>
    <item>
      <title>Stop Taking Down Production Systems. Use "Blast Radius" To Help You.</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Sun, 28 Jun 2026 19:59:06 +0000</pubDate>
      <link>https://dev.to/aws-builders/terraform-plan-shows-what-youre-changing-blast-radius-shows-what-youre-breaking-3324</link>
      <guid>https://dev.to/aws-builders/terraform-plan-shows-what-youre-changing-blast-radius-shows-what-youre-breaking-3324</guid>
      <description>&lt;p&gt;&lt;em&gt;Series: Introducing Blast Radius — See What Breaks Before You Deploy&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  “It Was Just a Security Group Change”
&lt;/h2&gt;

&lt;p&gt;A developer modifies an ingress rule on a security group. The pull request looks clean, and &lt;code&gt;cdk diff&lt;/code&gt; shows that only one resource is changing. The PR is approved, merged, and deployed.&lt;/p&gt;

&lt;p&gt;Then the alerts start flowing in. Ten EC2 instances lose connectivity. Three RDS databases become unreachable. The production app goes down.&lt;/p&gt;

&lt;p&gt;The change was correct in isolation. The problem was everything downstream of that change, and nothing in the IaC diff revealed it.&lt;/p&gt;

&lt;p&gt;This isn’t a fictional horror story. It is a category of incident that happens because IaC tools are blind to runtime dependency relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Terraform Plan Shows Your Change — Not Its Consequences.
&lt;/h2&gt;

&lt;p&gt;IaC tools tell us what we are changing, but they don’t tell us what else is affected and will break.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; shows 1 resource modified. In reality, 28 downstream resources are affected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdk diff&lt;/code&gt; shows a property update. In reality, there is a cascading dependency chain through Config relationships.&lt;/p&gt;

&lt;p&gt;CloudFormation change sets list actions on declared resources. Undeclared runtime dependencies are invisible.&lt;/p&gt;

&lt;p&gt;Manual review doesn’t scale. You’d need to trace relationships across AWS Config, Resource Explorer, and tribal knowledge, and still probably miss something. The other option is to deploy to a dev environment and see what breaks.&lt;/p&gt;

&lt;p&gt;The information exists in AWS. Config tracks relationships. Resource Explorer indexes resources. It’s just not connected to your deployment workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  See the Blast Radius Before You Deploy.
&lt;/h2&gt;

&lt;p&gt;Blast Radius connects the data AWS already has to your deployment workflow. It discovers downstream dependencies from your infrastructure change, scores each one for risk, and optionally asks AI whether you should deploy.&lt;/p&gt;

&lt;p&gt;It works with CDK, CloudFormation, and Terraform out of the box. You don’t need to change your IaC tool.&lt;/p&gt;

&lt;p&gt;The dependency graph makes the cascade visible: your changed resource highlighted in blue, everything downstream fanning out and color-coded by risk score. Click any node for details. There’s a table view too.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5tzucbh8umcdja0ziyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5tzucbh8umcdja0ziyh.png" alt="Frontend Dependency Graph Example" width="800" height="644"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Frontend Dependency Graph Example&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The score isn’t a mystery: &lt;code&gt;(depth × 0.30) + (criticality × 0.40) + (change severity × 0.30)&lt;/code&gt;. It is explainable, auditable, and tunable.&lt;/p&gt;

&lt;p&gt;The output lands in your PR as a comment (GitHub Action), in your terminal (CLI), or in an interactive graph (frontend).&lt;/p&gt;
&lt;h2&gt;
  
  
  From IaC Diff to Risk Score in 30 Seconds
&lt;/h2&gt;

&lt;p&gt;You push a PR with an infrastructure change.&lt;/p&gt;

&lt;p&gt;Blast Radius auto-generates the changeset (runs &lt;code&gt;cdk synth&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt;, or reads your CloudFormation change set).&lt;/p&gt;

&lt;p&gt;An adapter normalizes the changeset into a single canonical format. CDK, CloudFormation, and Terraform all describe the same change in different ways. The canonical format unifies them.&lt;/p&gt;

&lt;p&gt;AWS Config queries discover real, live dependency relationships. Not what’s in the IaC, what’s actually wired together in your account.&lt;/p&gt;

&lt;p&gt;Each affected resource is scored for how far away it is (depth), how important it is (criticality), and how dangerous the change type is (severity).&lt;/p&gt;

&lt;p&gt;Results: a risk summary, scored resources, dependency graph, and optional AI-generated deployment recommendation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2yu5bt1cux7tdf5u62k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo2yu5bt1cux7tdf5u62k.png" alt="Flow Diagram of how Blast Radius Works" width="800" height="211"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Flow Diagram of how Blast Radius Works&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Total wall time: 10–30 seconds, depending on graph size.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two Gates: Numbers and Judgment
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Threshold gate (&lt;code&gt;--threshold 75&lt;/code&gt;):&lt;/em&gt; If any downstream resource scores above your number, the pipeline fails. Deterministic. No AI required. Good for teams that want a hard cutoff.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;AI gate (&lt;code&gt;--ai-gate&lt;/code&gt;):&lt;/em&gt; An AI model analyzes the full dependency graph and returns a deploy/no-deploy recommendation with confidence. Catches systemic risks that individually score below the threshold but are dangerous collectively.&lt;/p&gt;

&lt;p&gt;You modify a security group attached to a shared RDS Proxy. Eight Lambda functions score 62. Two ECS services score 68. All below your threshold of 75. But every one of them routes database traffic through that proxy. Take it down and you’ve killed every service that touches your database. The AI sees the shared dependency. The threshold just sees ten numbers under 75.&lt;/p&gt;

&lt;p&gt;You can use either of them or both together. If using both, it fails if EITHER triggers. This gives you defense in depth.&lt;/p&gt;

&lt;p&gt;Exit codes are CI-friendly: 0 = pass, 1 = fail, 2 = error&lt;/p&gt;
&lt;h2&gt;
  
  
  Add It to Your Pipeline in Five Minutes
&lt;/h2&gt;

&lt;p&gt;First, deploy the backend to your own AWS account. One stack, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sburgholzer/BlastRadius.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;BlastRadius
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;packages/infra &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; cdk deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One requirement before you deploy: AWS Config and Resource Explorer must be enabled in your account. That’s where Blast Radius reads live dependency relationships.&lt;/p&gt;

&lt;p&gt;Then add it to your pipeline. The fastest path is the GitHub Action — it auto-comments on your PR with the full analysis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sburgholzer/BlastRadius@v0.1.0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;terraform-plan&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan.json&lt;/span&gt;
    &lt;span class="na"&gt;ai-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;api-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BLAST_RADIUS_URL }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CDK, you’ll need to generate a CloudFormation changeset first, then pass it to the action. The full setup is in Article 2.&lt;/p&gt;

&lt;p&gt;Or skip the GitHub Action entirely and run it from the CLI in any CI system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sL&lt;/span&gt; https://github.com/sburgholzer/BlastRadius/releases/latest/download/blast-radius.js &lt;span class="nt"&gt;-o&lt;/span&gt; blast-radius.js
node blast-radius.js analyze &lt;span class="nt"&gt;--format&lt;/span&gt; cdk &lt;span class="nt"&gt;--stack&lt;/span&gt; MyStack &lt;span class="nt"&gt;--ai-gate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Blast Radius is open source. &lt;a href="https://github.com/sburgholzer/BlastRadius" rel="noopener noreferrer"&gt;You can find the repo here.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;Infrastructure changes will always carry risk. The goal isn’t to eliminate that risk; it’s to see it clearly before you deploy. Blast Radius gives you that visibility in the time it takes to review a PR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next In This Series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Article 2: Stop Deploying Blind: Add a Blast Radius Gate to Your CI/CD Pipeline&lt;/strong&gt;: Step-by-step setup for GitHub Actions, GitLab, and Jenkins. Choosing between threshold and AI gates, tuning your cutoff, and reading the output like a pro.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Article 3: Serverless IaC Risk Analysis: The Architecture Behind Blast Radius&lt;/strong&gt;: How the pipeline is designed. Why Step Functions. How the canonical format abstraction makes multi-IaC support possible without touching the analysis engine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Article 4: 349 Tests, Zero Mocks: Building Blast Radius in TypeScript&lt;/strong&gt;: Engineering decisions, property-based testing with fast-check, dependency injection over module mocking, and the Lambda runtime gotchas that wasted a day.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;These articles are coming in the next few weeks. I’ll update this post with links as each one publishes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Title help from Aaron Hunter!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>infrastructureascode</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>The InvalidCiphertextException Mystery: Decrypting Cognito's Encrypted OTP Codes</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Thu, 01 Jan 2026 05:05:01 +0000</pubDate>
      <link>https://dev.to/aws-builders/the-invalidciphertextexception-mystery-decrypting-cognitos-encrypted-otp-codes-jg8</link>
      <guid>https://dev.to/aws-builders/the-invalidciphertextexception-mystery-decrypting-cognitos-encrypted-otp-codes-jg8</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You're building a custom email sender for Cognito because the default emails are pretty basic. In this case you want branded HTML emails for a One Time Password code that actually explain what the code is for and when it expires. Sounds straightforward, right?&lt;/p&gt;

&lt;p&gt;Then you hit the encryption wall. Cognito encrypts those OTP codes for security (which is great), but when you try to decrypt them in your Lambda function using standard KMS operations, you get this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InvalidCiphertextException: An error occurred (InvalidCiphertextException) when calling the Decrypt operation:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No helpful details, no explanation of what went wrong. The error message makes it seem like there's something fundamentally wrong with the ciphertext, but even after triple-checking your KMS permissions, it still doesn't work. Here's why, and more importantly, how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR - The Solution
&lt;/h2&gt;

&lt;p&gt;Cognito doesn't use regular KMS encryption. It uses the &lt;strong&gt;AWS Encryption SDK&lt;/strong&gt;, which creates a completely different data structure. You need to use the same AWS Encryption SDK (v4 with Material Providers Library) to decrypt the codes. Jump to the working code if you just want the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Everyone Tries First, Including Myself (And Why It Fails)
&lt;/h2&gt;

&lt;p&gt;When you think AWS encryption, you think KMS. So naturally, you try the standard approaches:&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 1: Basic KMS Decryption
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# This seems logical but doesn't work
&lt;/span&gt;&lt;span class="n"&gt;kms_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;decrypt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kms_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CiphertextBlob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;encrypted_blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; &lt;code&gt;InvalidCiphertextException&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 2: Adding Encryption Context
&lt;/h3&gt;

&lt;p&gt;Maybe it needs the user pool ID as context?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Still doesn't work
&lt;/span&gt;&lt;span class="n"&gt;decrypt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kms_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CiphertextBlob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;encrypted_blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EncryptionContext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userpool-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;userPoolId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Same error&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 3: AWS-Prefixed Context
&lt;/h3&gt;

&lt;p&gt;Perhaps it needs the AWS service prefix?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nope, still fails
&lt;/span&gt;&lt;span class="n"&gt;decrypt_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kms_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;CiphertextBlob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;encrypted_blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EncryptionContext&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws:cognito:userpool-id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;userPoolId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Still the same error&lt;/p&gt;

&lt;p&gt;At this point, you start questioning everything. Your KMS permissions look right, the key exists, but nothing works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Issue: It's Not Actually KMS Ciphertext
&lt;/h2&gt;

&lt;p&gt;Here's the key insight that changes everything: &lt;strong&gt;Cognito doesn't use direct KMS encryption&lt;/strong&gt;. Instead, it uses something called the AWS Encryption SDK, which creates a completely different type of encrypted data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's the Difference?
&lt;/h3&gt;

&lt;p&gt;Think of it this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Regular KMS encryption&lt;/strong&gt; = A locked box&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Encryption SDK&lt;/strong&gt; = A locked box inside a shipping container with labels, tracking info, and handling instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you try to use KMS to "unlock" the shipping container, KMS says "I don't know what this is - this isn't a box I locked!"&lt;/p&gt;

&lt;h3&gt;
  
  
  The Technical Details
&lt;/h3&gt;

&lt;p&gt;When Cognito encrypts an OTP code, here's what actually happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cognito uses the AWS Encryption SDK&lt;/strong&gt; (not direct KMS)&lt;/li&gt;
&lt;li&gt;The SDK creates an "envelope" containing:

&lt;ul&gt;
&lt;li&gt;Algorithm information&lt;/li&gt;
&lt;li&gt;An encrypted data key (this part uses KMS)&lt;/li&gt;
&lt;li&gt;The actual encrypted OTP code&lt;/li&gt;
&lt;li&gt;Integrity checks and metadata&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;This entire envelope gets passed to your Lambda function&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;encrypted_blob&lt;/code&gt; you receive isn't simple KMS ciphertext - it's this complex envelope structure. That's why KMS can't decrypt it directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the Error Message is Confusing
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;InvalidCiphertextException&lt;/code&gt; error just says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;An error occurred (InvalidCiphertextException) when calling the Decrypt operation:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No details, no explanation. This generic message makes you think the ciphertext is corrupted or you have permission issues, but the real problem is that KMS is saying "I can't even parse this data structure." &lt;/p&gt;

&lt;p&gt;It's like trying to open a ZIP file with a text editor - the format is just wrong, but the error message doesn't tell you that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: AWS Encryption SDK Implementation
&lt;/h2&gt;

&lt;p&gt;Once you understand the problem, the solution becomes clear. You need to use the same AWS Encryption SDK that Cognito uses to create the encrypted data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What You Need
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AWS Encryption SDK v4&lt;/strong&gt; - The encryption library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Material Providers Library (MPL)&lt;/strong&gt; - Required for v4&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proper KMS permissions&lt;/strong&gt; - Your Lambda still needs to access the KMS key&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The right Python packages&lt;/strong&gt; - Install them as a Lambda layer&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Working Code
&lt;/h3&gt;

&lt;p&gt;Here's the code that actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aws_encryption_sdk&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_encryption_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CommitmentPolicy&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cryptographic_material_providers.mpl&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AwsCryptographicMaterialProviders&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cryptographic_material_providers.mpl.config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MaterialProvidersConfig&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cryptographic_material_providers.mpl.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CreateAwsKmsKeyringInput&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decrypt_cognito_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted_blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Set up the encryption client
&lt;/span&gt;    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aws_encryption_sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EncryptionSDKClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;commitment_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CommitmentPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REQUIRE_ENCRYPT_ALLOW_DECRYPT&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Create the material providers
&lt;/span&gt;    &lt;span class="n"&gt;mat_prov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AwsCryptographicMaterialProviders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;MaterialProvidersConfig&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Set up the KMS keyring
&lt;/span&gt;    &lt;span class="n"&gt;kms_key_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;keyring_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CreateAwsKmsKeyringInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;kms_key_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;kms_key_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;kms_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kms&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;kms_keyring&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mat_prov&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_aws_kms_keyring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;keyring_input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Now this actually works!
&lt;/span&gt;    &lt;span class="n"&gt;plaintext_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decryption_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;encrypted_blob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;keyring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;kms_keyring&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Convert to string and return
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;plaintext_bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting It Up
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Install the packages:&lt;/strong&gt;&lt;br&gt;
Create a Lambda layer with these dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"aws-encryption-sdk[MPL]"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;4.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Set your environment variable:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KMS_KEY_ID=your-kms-key-id-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Update your IAM role:&lt;/strong&gt;&lt;br&gt;
Your Lambda execution role needs &lt;code&gt;kms:Decrypt&lt;/code&gt; permission for your KMS key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"kms:Decrypt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:kms:region:account:key/your-key-id"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Matters Beyond Cognito
&lt;/h2&gt;

&lt;p&gt;This isn't just a Cognito quirk. Other AWS services also use the AWS Encryption SDK instead of direct KMS encryption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Systems Manager Parameter Store&lt;/strong&gt; (SecureString parameters)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Some S3 client-side encryption&lt;/strong&gt; scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom applications&lt;/strong&gt; that need to encrypt large amounts of data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is becoming more common because the AWS Encryption SDK offers benefits that direct KMS doesn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No size limits&lt;/strong&gt; (KMS is limited to 4KB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better performance&lt;/strong&gt; for large data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additional security features&lt;/strong&gt; like key commitment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Key Takeaway
&lt;/h3&gt;

&lt;p&gt;When you see an AWS service that says it "uses KMS encryption," don't assume it's using direct KMS calls. It might be using the AWS Encryption SDK under the hood, which changes everything about how you decrypt the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting Tips
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're still getting errors:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check your Lambda layer&lt;/strong&gt; - Make sure you have the right packages installed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify your KMS key ID&lt;/strong&gt; - It should be the same one configured in your Cognito user pool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test your IAM permissions&lt;/strong&gt; - Try a simple KMS decrypt operation to verify access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the commitment policy&lt;/strong&gt; - Use &lt;code&gt;REQUIRE_ENCRYPT_ALLOW_DECRYPT&lt;/code&gt; for compatibility&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Common mistakes:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using the wrong version of the AWS Encryption SDK (you need v4)&lt;/li&gt;
&lt;li&gt;Forgetting to install the Material Providers Library&lt;/li&gt;
&lt;li&gt;Using the wrong KMS key (it must match what Cognito is configured to use)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Error message reference:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;InvalidCiphertextException&lt;/code&gt; with no details = You're trying to decrypt AWS Encryption SDK data with direct KMS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IncorrectKeyException&lt;/code&gt; = You're using the wrong KMS key for real KMS ciphertext&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AccessDeniedException&lt;/code&gt; = Actual permissions problem&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This problem can stump a lot of developers because the error message is so generic and unhelpful. When you see &lt;code&gt;InvalidCiphertextException&lt;/code&gt; with no details, it's usually a sign that you're trying to decrypt AWS Encryption SDK data with direct KMS calls.&lt;/p&gt;

&lt;p&gt;Once you understand that Cognito uses the AWS Encryption SDK instead of direct KMS, everything clicks into place. The extra complexity is worth it - you get better security, no size limits, and future-proof encryption. Plus, now you know how to handle this pattern when you encounter it in other AWS services.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sender-triggers.html" rel="noopener noreferrer"&gt;AWS Cognito Custom Sender Lambda Triggers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/" rel="noopener noreferrer"&gt;AWS Encryption SDK Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/python-example-code.html" rel="noopener noreferrer"&gt;AWS Encryption SDK Python Examples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>encryption</category>
      <category>lambda</category>
      <category>kms</category>
    </item>
    <item>
      <title>Understanding Amazon CloudFront's New Flat-Rate Pricing</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Thu, 27 Nov 2025 00:45:35 +0000</pubDate>
      <link>https://dev.to/aws-builders/understanding-amazon-cloudfronts-new-flat-rate-pricing-e9k</link>
      <guid>https://dev.to/aws-builders/understanding-amazon-cloudfronts-new-flat-rate-pricing-e9k</guid>
      <description>&lt;p&gt;On November 18th, AWS introduced new flat‑rate pricing plans for Amazon CloudFront designed to make content delivery and security costs more predictable for teams of all sizes. These plans sit alongside the existing pay‑as‑you‑go model and bundle multiple services into a single monthly price per distribution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Flat-Rate Pricing&amp;nbsp;Matters
&lt;/h3&gt;

&lt;p&gt;Traditionally, CloudFront has used pay‑as‑you‑go pricing, which is great for starting at $0, scaling with actual usage, and only paying for what you consume. The tradeoff is that estimating costs can be difficult, especially when you also depend on AWS WAF, DDoS protection, Route 53, CloudWatch Logs, and S3 for a single application. You end up stitching together multiple pricing pages and trying to map them to your traffic patterns just to get a reasonable forecast of your monthly bill.&lt;/p&gt;

&lt;p&gt;The new flat‑rate plans aim to simplify this. Instead of tracking every request, rule, and log line, you choose a plan tier per distribution and pay a fixed monthly fee that includes a defined bundle of features and usage allowances.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Included in the New&amp;nbsp;Plans
&lt;/h3&gt;

&lt;p&gt;Each flat‑rate plan bundles the following into one monthly price per CloudFront distribution:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon CloudFront CDN&lt;/li&gt;
&lt;li&gt;AWS WAF and DDoS protection&lt;/li&gt;
&lt;li&gt;Bot management and analytics&lt;/li&gt;
&lt;li&gt;Amazon Route 53 DNS&lt;/li&gt;
&lt;li&gt;Amazon CloudWatch Logs ingestion&lt;/li&gt;
&lt;li&gt;Serverless edge compute (via CloudFront Functions)&lt;/li&gt;
&lt;li&gt;Monthly Amazon S3 storage credits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because everything is packaged together, you get a predictable bill for that distribution with no overage charges. There are no surprises at the end of the month.&lt;/p&gt;

&lt;p&gt;A few structural details to keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Plans are per distribution. If you put two distributions on the Business plan at $200 each, you will pay $400 per month for those two.&lt;/li&gt;
&lt;li&gt;You can upgrade at any time, but downgrades/cancellations or a move back to pay‑as‑you‑go only take effect at the start of the next billing cycle.&lt;/li&gt;
&lt;li&gt;There is a limit of three free plans per AWS account and a limit of 100 total plans per account, which matters if you run many environments or tenants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh2xx8sfav7ijcbwv7tzt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh2xx8sfav7ijcbwv7tzt.png" alt="CloudFront flat-rate plans" width="800" height="599"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling Attacks and Traffic&amp;nbsp;Spikes
&lt;/h3&gt;

&lt;p&gt;One of the more reassuring aspects of these plans is how they treat malicious traffic. DDoS attacks do not count against your usage allowance, and traffic blocked by AWS WAF or bot protections also does not consume your plan quota. That means an attack should not cause you to exceed your allowance.&lt;/p&gt;

&lt;p&gt;You will receive email notifications when you hit 50%, 80%, and 100% of your usage allowance for the month. If you exceed the allowance, AWS will not tack on overage fees, but you may experience reduced performance. That performance reduction is at AWS's discretion and depends on overall network conditions and other factors, so consistently bumping into your limits is a good signal that it is time to move to a higher‑tier plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edge Compute: CloudFront Functions vs. Lambda@Edge
&lt;/h3&gt;

&lt;p&gt;All of the flat‑rate plans include CloudFront Functions, which are ideal for lightweight JavaScript logic at the edge such as header manipulation, redirects, and simple request or response normalization. For many use cases, this will cover the bulk of what you might previously have used Lambda@Edge for.&lt;/p&gt;

&lt;p&gt;However, Lambda@Edge is not supported on the flat‑rate plans. If your distribution relies on Lambda@Edge, such as more complex request processing, integrations, or heavy transformations, you must keep that distribution on the pay‑as‑you‑go pricing model. This is an important constraint to verify before flipping an existing production distribution over to a flat‑rate plan.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pay-As-You-Go Is Still Available
&lt;/h3&gt;

&lt;p&gt;All of this arrives in addition to, not instead of, the existing pay‑as‑you‑go model. The traditional CloudFront pricing structure remains available with its free tier, which is still a solid option for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Very low‑traffic or experimental workloads&lt;/li&gt;
&lt;li&gt;Architectures that depend on Lambda@Edge or other features not supported in flat‑rate plans&lt;/li&gt;
&lt;li&gt;Teams that prefer pure usage‑based billing and are comfortable modeling and monitoring their costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, many organizations will likely end up with a mix: flat‑rate plans for high‑volume, business‑critical sites where predictability matters, and pay‑as‑you‑go for specialized or experimental distributions.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudfront</category>
      <category>cdn</category>
    </item>
    <item>
      <title>What is AWS Device Farm and Using it to Test a Simple Android App</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Sat, 23 Nov 2024 04:37:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/what-is-aws-device-farm-and-using-it-to-test-a-simple-android-app-ah7</link>
      <guid>https://dev.to/aws-builders/what-is-aws-device-farm-and-using-it-to-test-a-simple-android-app-ah7</guid>
      <description>&lt;h4&gt;
  
  
  Introduction
&lt;/h4&gt;

&lt;p&gt;As I have been busy with my classes for my doctorate and work, I haven’t had much time to write articles like I had hoped. I was hoping to have a project done that I could discuss how I created a Web App using multiple AWS services for others to learn how they all can interact. As that project is still ongoing, I decided to focus on something else.&lt;/p&gt;

&lt;p&gt;I choose AWS Device Farm because eventually I would need to test that web app on mobile devices and on desktop browsers. Device Farm allows us to use physical devices to test our apps. Not only can we test web apps, we can also test Android and iOS apps on physical devices. There is a lot to learn for Device Farm, so I started very basic with using it, and hence this article was born. I hope, while this article just uses a simple app and the defaults, it can expose more people to this AWS service and see if it’ll work in their use cases.&lt;/p&gt;

&lt;p&gt;View this article as a “I’ve never heard or seen AWS Device Farm, so I want to start from the basics and work my way up” as that is exactly what I’m doing with you!&lt;/p&gt;




&lt;h4&gt;
  
  
  What is AWS Device&amp;nbsp;Farm?
&lt;/h4&gt;

&lt;p&gt;AWS Device farm is a cloud-based testing platform that allows developers to test their mobile and web apps on physical devices instead of simulators. There are two main ways of using Device Farm: automated testing of apps and remote access of devices [1].&lt;/p&gt;

&lt;h4&gt;
  
  
  Automated app&amp;nbsp;testing
&lt;/h4&gt;

&lt;p&gt;AWS Device Farm allows developers the ability to upload their own tests or to use built-in and script-free compatibility. AWS Device Farm performs testing in parallel, tests on multiple devices begin in minutes. As these tests are completed, a report is generated that contains high-level results, low-level logs, pixel-to-pixel screenshots and performance data. Device Farm supports the testing of native and hybrid Android and iOS Apps. Some frameworks that work with Device Farm are PhoneGap, Titanium, Xamarin, Unity, and others. You also get remote access of Android and iOS apps for interactive testing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Remote access interaction
&lt;/h4&gt;

&lt;p&gt;This allows you to swipe, gesture and interact with a device through your web browser in real time. An example of where this is beneficial is a customer service rep can guide customers through the use or setup of their device. You have the ability to install apps on a device running in a remote access session and reproduce bugs or other issues. While the session is live, Device Farm will collect details about the actions that are completed on the device. Logs with those details and a video capture of the session are available at the end of the session [1].&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note about regions:&lt;/strong&gt; Per the documentation, Device Farm is only available in the us-west-2 (Oregon) region [1].&lt;/p&gt;




&lt;h4&gt;
  
  
  Walk-through
&lt;/h4&gt;

&lt;p&gt;I’m going to skip over the setting up section. If you wish to go through it in detail (signing up for an account, create or use an IAM user) you may read that &lt;a href="https://docs.aws.amazon.com/devicefarm/latest/developerguide/setting-up.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Below is an example IAM policy [1] to allow full access to Device Farm. For this walk-through, this is fine, but if you are going to implement this service, you are going to want to restrict it as much as you can to the least privilege access.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"devicefarm:*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuzi74i1y13x4f5rb9ho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbuzi74i1y13x4f5rb9ho.png" alt="Device Farm Console" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Device Farm&amp;nbsp;Console&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a project&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to Device Farm in the console
&lt;/li&gt;
&lt;li&gt;Expand the left hand navigation if necessary
&lt;/li&gt;
&lt;li&gt;Click on Projects underneath Mobile Device Testing
&lt;/li&gt;
&lt;li&gt;Click on Create mobile project
&lt;/li&gt;
&lt;li&gt;Enter a project name and assign any tags you wish the project to have
&lt;/li&gt;
&lt;li&gt;Click Create&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create and start a run&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You will need an app in order to start the run. I have created a very simple Android App on my &lt;a href="https://github.com/sburgholzer/AWS-Device-Farm-Examples-for-Blog/blob/main/app-debug.apk" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Go ahead and download that if you do not have your own Android or Apple App (note in order to get an&amp;nbsp;.ipa file for iOS you will need to have an Apple Developer Account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stay on the Automated tests page and click on Create Run  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Device Farm uses the app name as the default run name, feel free to choose your own name! Click Next  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You’ll be taken to a configure page to setup your test framework. You have the ability to choose one of the testing frameworks provided or built-in test suites. Since we are not worrying about our own tests for this example, we can use the Built-In: Fuzz for our case (can keep all the defaults). When you start writing your own test packages, you can choose the corresponding testing framework and upload the file with your tests. This is not applicable in our example. Click Next  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For our example, we will use the Top Devices Device pool. You do have the ability to create your own device pools, but for our simple app, Top Devices is just fine. Click Next  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We are going to keep the next page as is. You can read more about the options in the Developer Guide, which you can find at the end of this article tagged [1]. Click Next  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We will leave the execution timeout to the default value provided and will then click Confirm and start run. This can take a while, so wait patiently.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdmygejisgotr8twl5ih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwdmygejisgotr8twl5ih.png" alt="Run Results" width="799" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Run Results&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: View the run’s results&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
 — When the run is done, you will see the results of the runs. In my case 3 tests were ran and all passed.&lt;br&gt;&lt;br&gt;
 — The unique problems would list any problems that were discovered. A video can be downloaded to view the recording of the test that had the problem. There are additional data provided that I won’t get into detail in this article.&lt;br&gt;&lt;br&gt;
 — Screenshots will display any screenshots Device Farm took during the run.&lt;/p&gt;




&lt;h4&gt;
  
  
  Where to go from&amp;nbsp;here?
&lt;/h4&gt;

&lt;p&gt;The next thing I personally would suggest, and plan on doing, is to learn more about test packages. There are frameworks that work with both iOS and Android, and some that only work for only Android or only iOS. This is where you are able to specify your tests the way you want them instead of using just the built-in tests AWS provides. This gives you complete control over how you want the tests done and what you want tested. This is where the real power of AWS Device Farm comes into play.&lt;/p&gt;

&lt;p&gt;The other thing to look into is the Desktop Browser Testing for any web apps. Personally for me, my project is going to start as just a web app, so Desktop Browser Testing is of personal importance. As always, you need to figure out your use cases and determine what your needs are.&lt;/p&gt;

&lt;p&gt;There is so much more to learn about AWS Device Farm, and I will continue posting about it as I go through it and learn it myself. For now, happy developing and testing!&lt;/p&gt;

&lt;p&gt;[1] &lt;a href="https://docs.aws.amazon.com/devicefarm/latest/developerguide/welcome.html" rel="noopener noreferrer"&gt;AWS Device Farm Developer Guide&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devicefarm</category>
      <category>testingtools</category>
    </item>
    <item>
      <title>Launching a simple EC2 Instance with Apache Web Server — Part 1</title>
      <dc:creator>Scott Burgholzer</dc:creator>
      <pubDate>Mon, 11 Mar 2024 02:06:15 +0000</pubDate>
      <link>https://dev.to/aws-builders/launching-a-simple-ec2-instance-with-apache-web-server-part-1-33nl</link>
      <guid>https://dev.to/aws-builders/launching-a-simple-ec2-instance-with-apache-web-server-part-1-33nl</guid>
      <description>&lt;p&gt;&lt;strong&gt;Little backstory:&lt;/strong&gt; I’ve had this in my drafts for a while, wanting to do it all as one article. I’ve decided that is not the best way of presenting this information. As a newer content creator, I’ve been playing with how I want my pages to look like, and have decided to go with how it looks right now.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, I will be doing what some call “click-ops” in AWS console as that is the easiest way to get started. If you want more challenges, I will also be posting on how to use AWS CLI (an article post on this will come at a later date, when completed, I will be updating this with a link to it!) to do the same thing. I will also show how to use a Cloud Development Kit (CDK). I will be using Python in my CDK examples. I will also use AWS CloudFormation. Finally, for an even more advanced challenge, I will be creating this process again using Terraform. If you are new, the console way will be the easiest, but I also want to introduce you to the CLI, CDK, CloudFormation and Terraform as these are advanced tools used!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Scope of this tutorial&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this multi-part tutorial, we are wanting to set up an EC2 instance, configure security group(s), create a key-pair SSH Key to remote into the instance, and install Apache Web Server.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Method 1:&lt;/strong&gt; Using AWS Console (without using userdata)&lt;/p&gt;

&lt;p&gt;There are actually two ways we can use the AWS console to create the EC2 instance, and install Apache and run it and create the test HTML file. This method will require us to use SSH to remote into our EC2 instance to preform some steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; When you are logged into the AWS Console, search for EC2 in the search bar.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4zyev7c4psyzew0g3qc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4zyev7c4psyzew0g3qc.png" alt="AWS Console and searching for EC2 via the search bar." width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Click on Launch Instance&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foc5cveduvfa026ric38d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foc5cveduvfa026ric38d.png" alt="EC2 console dashboard with Launch Instance button shown" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Configure your instance and install Apache and test web page&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Give the instance a name, I called mine “Web Server Test”    * Keep the application and OS Images as is&lt;/li&gt;
&lt;li&gt;Keep the instance type as t2.micro&lt;/li&gt;
&lt;li&gt;Click on new key par and give it a name, then click on Create key pair. It’ll download a file, move it to a safe location so you don’t loose it!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hh4oktrmp3ybdz4tamw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0hh4oktrmp3ybdz4tamw.png" alt="Creating a Key Pair" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under Network settings we will be keeping the selection Create Security Group. Allow SSH traffic from should be checked, and you’ll also want to check the Allow HTTP traffic from the internet. Next to SSH there is a dropdown menu that you can say any IP can connect to the instance, a custom IP or range, or your IP only. For this example, I’m keeping it as anywhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qkjbaspm7g42ml0noft.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qkjbaspm7g42ml0noft.png" alt="Creating the security group through the Launch Instance Wizard" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We will leave everything else as default&lt;/li&gt;
&lt;li&gt;Click on Launch Instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdmu9ixhn5z1q1aojh66.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcdmu9ixhn5z1q1aojh66.png" alt="showing the instance was created successfully from the wizard with a green success box with a link to view that instance." width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click on the instance ID shown in the green success box&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f8tylkz28vctdvo66h7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2f8tylkz28vctdvo66h7.png" alt="Instance shown as running" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Click the checkbox next to the instance, then go to actions and click on Connect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6heinfunm0n0qa33im0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu6heinfunm0n0qa33im0.png" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You’ll see there are four options of connecting. The only two that will work with our instance, due to the way we set it up, is EC2 Instance Connect and SSH client. If on Mac or Linux you can use command line to SSH into the instance’s command line, on Windows it is easiest to use a tool such as Putty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz3upox6kyi44xmu35e7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flz3upox6kyi44xmu35e7.png" alt="Showing the connect options, and the EC2 Instance Connect we are using in our example" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We will be using EC2 Instance connect in our case, so click on that tab, then click on connect&lt;/li&gt;
&lt;li&gt;After waiting a few moments, you’ll see a command line terminal for our instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu32gmo2qevknel0gch3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu32gmo2qevknel0gch3g.png" alt="The command line interface via EC2 Instance Connect" width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We now want to install Apache and start the service. Run the following commands in the command prompt.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;yum update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="c"&gt;# We are asking the packacge manager to install any updates&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;yum &lt;span class="nb"&gt;install &lt;/span&gt;httpd &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="c"&gt;# We are asking the package manager to install Apache&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;httpd &lt;span class="c"&gt;# We are telling the server to automatically start Apache upon start up&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start httpd &lt;span class="c"&gt;# we are telling the server to start the Apache Server&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status httpd &lt;span class="c"&gt;# we are telling the server to tell us the status of the Apache Server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvehd68fidaww8mxccfy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmvehd68fidaww8mxccfy.png" alt="Results of some of the commands and showing that Apache is running" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We are going to create a simple HTML file, run the below command to open a new file using a command line text editor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;sudo nano /var/www/html/index.html&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put the following code into the text editor
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Test Web Page&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Welcome to my website!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;I love AWS&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;When you are done with the file, press ctl and x, it’ll prompt you if you want to save, type Y, press enter, then press enter again.&lt;/li&gt;
&lt;li&gt;Go back to the EC2 Instances page, click on your instance, then click on open address under the Public IPv4 address, change the HTTPS to HTTP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ngmx3ft4etn2f5upp5t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ngmx3ft4etn2f5upp5t.png" alt="You should see your test page" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you see your test page, you were successful!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Clean Up&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Close all windows except for the Instances window.&lt;/li&gt;
&lt;li&gt;Select it, then click on Instance state then click on Terminate instance so you don’t use up your free trial credits or get charged!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Congrats! You have just launched a simple EC2 Web server!&lt;/p&gt;




&lt;p&gt;&lt;a href="https://dev.to/aws-builders/launching-a-simple-ec2-instance-with-apache-web-server-part-1-33nl"&gt;Launching a simple EC2 Instance with Apache Web Server — Part 1 (AWS Console)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Launching a simple EC2 Instance with Apache Web Server — Part 2 (AWS Console with Userdata) (Coming Soon)&lt;/p&gt;

&lt;p&gt;Launching a simple EC2 Instance with Apache Web Server — Part 3 (AWS CLI with user data) (Coming Soon)&lt;/p&gt;

&lt;p&gt;Launching a simple EC2 Instance with Apache Web Server — Part 4 (Terraform with user data) (Coming Soon)&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ec2</category>
    </item>
  </channel>
</rss>
