<?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: Carlos Filho</title>
    <description>The latest articles on DEV Community by Carlos Filho (@carlosfilho).</description>
    <link>https://dev.to/carlosfilho</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%2F1354865%2Fe88bd032-5bca-45d6-bc33-e04dbb8ff160.png</url>
      <title>DEV Community: Carlos Filho</title>
      <link>https://dev.to/carlosfilho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlosfilho"/>
    <language>en</language>
    <item>
      <title>Scheduling weekly SNS notifications with Amazon EventBridge</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Mon, 19 Oct 2026 21:07:39 +0000</pubDate>
      <link>https://dev.to/aws-builders/scheduling-weekly-sns-notifications-with-amazon-eventbridge-d93</link>
      <guid>https://dev.to/aws-builders/scheduling-weekly-sns-notifications-with-amazon-eventbridge-d93</guid>
      <description>&lt;p&gt;A manual weekly SNS campaign was replaced by a scheduled EventBridge trigger in two CloudFormation templates: an Events rule in UTC and a Scheduler in local time.&lt;/p&gt;

&lt;p&gt;Every Friday around 11 PM, someone on the marketing team of a retail client opened the Amazon SNS console, pasted the Saturday campaign copy into the message field and clicked publish. The topic carried a few million confirmed subscriptions, in a mix of email, SMS, mobile push and one SQS queue feeding an internal consumer. The procedure lived in a shared document, with the topic ARN in bold and an all-caps warning to confirm the region before publishing.&lt;/p&gt;

&lt;p&gt;What brought me in was not a push for automation. It was an incident. On two consecutive Saturdays the campaign went out twice. The first time, the person published, the page stopped responding, and she published again. The second time, two people each assumed the other was on vacation. Between those two episodes there was a Saturday when the campaign did not go out at all, and nobody noticed until Monday, when the conversion report came back at zero.&lt;/p&gt;

&lt;p&gt;What I found when I opened the account: no alarms, and no record of who published what beyond raw CloudTrail. The delivery mechanism worked fine. The operational knowledge sat with two people, and the task runs 52 times a year, always at the same hour, finishing in under a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  What had to change, and what could not
&lt;/h2&gt;

&lt;p&gt;I needed a reliable weekly trigger, with no server to maintain, that leaves a trace of every firing. The delivery mechanism was not up for discussion: the SNS topic, the confirmed subscriptions and the endpoints stay exactly as they are. Any design that would require re-enrolling millions of subscribers was out before we started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Requirements for the weekly trigger
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The firing happens with no human in the loop, on Saturday, at a fixed local time in Sao Paulo. The hour itself moved: the manual ritual published late on Friday night so the message would be waiting when subscribers woke up, and the marketing team agreed to 09:00 on Saturday instead, which is a better hour for the campaign and a far easier one to reason about in a cron expression.&lt;/li&gt;
&lt;li&gt;No new compute resource to maintain, patch or monitor.&lt;/li&gt;
&lt;li&gt;The existing SNS topic stays as the delivery point, with its subscriptions untouched.&lt;/li&gt;
&lt;li&gt;The topic supports subscriptions in the order of millions, and the scheduling layer cannot be the bottleneck.&lt;/li&gt;
&lt;li&gt;A failed firing produces recoverable evidence, with payload and error code, instead of disappearing.&lt;/li&gt;
&lt;li&gt;No principal outside the account can publish to the campaign topic.&lt;/li&gt;
&lt;li&gt;The scheduling layer carries no fixed hourly cost, so the time it sits idle between firings is not billed.&lt;/li&gt;
&lt;li&gt;Every firing is observable through a metric and an alarm, without depending on someone watching.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four designs for the same weekly trigger
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cron on a dedicated EC2 instance
&lt;/h3&gt;

&lt;p&gt;This is the design anyone with an infrastructure background reaches for first, and it does work. The team already knows how to write a script and how to edit a crontab. The script can do anything before publishing, including querying a database inside the VPC to assemble the message body. There is no learning curve, and there is full control over the payload.&lt;/p&gt;

&lt;p&gt;What breaks is the host. A single instance sits on the critical path of a weekly campaign. If that instance is stopped, degraded, halfway through a reboot, or replaced by an Auto Scaling event during the scheduled minute, the cron entry does not fire. Linux cron keeps no memory of a missed run: if the instance came up at 9:05, the 9:00 job does not run, and nothing reports the miss. That is a Reliability failure, and it is the decisive one here, because it is the exact mechanism behind the Saturday that the client discovered on Monday. It comes paired with an Operational Excellence failure: someone has to patch the host, own a script nobody reviews, and the audit trail of who fired what is whatever the script happens to log. There is a Security failure hanging off the same host whenever the credential arrives as a long-lived access key instead of an instance profile, which is how I have most often found these instances configured, and I count it as a qualified risk rather than a defect of the design, because nothing forces the credential to arrive that way.&lt;/p&gt;

&lt;p&gt;The obvious mitigation trades one problem for another. Putting the instance in an Auto Scaling group with capacity 1 fixes availability and introduces duplicate firing during a replacement, when two instances can briefly carry the same crontab. Duplicate firing is the complaint that opened this work.&lt;/p&gt;

&lt;p&gt;Cost and sustainability come after, and they are consequence rather than cause. The instance stays up 168 hours a week to run a command that takes less than a second, once a week. The utilization rate is in the order of 0.0001 percent, which is a Cost Optimization failure in the form of a resource billed by time for work measured in milliseconds, and a Sustainability failure in the form of allocated hardware sitting almost entirely idle. Neither of those is the reason to reject the design. They are what the design costs after it already failed on reliability.&lt;/p&gt;

&lt;p&gt;An instance starts to earn its place when campaign preparation needs state, a heavy library, or access to something that exists only inside the VPC. That was not the case here. The message body was text.&lt;/p&gt;

&lt;h3&gt;
  
  
  A fanout subscription on the existing topic
&lt;/h3&gt;

&lt;p&gt;Fanout, in SNS, is the topology of one topic with multiple subscribers receiving the same message in parallel: SQS queues, Lambda functions, HTTP and HTTPS endpoints, email, SMS, mobile push. It is a correct and useful pattern, and it is exactly what this client already ran in production. That is what makes the proposal hard to dismiss on sight: the vocabulary is right.&lt;/p&gt;

&lt;p&gt;Fanout is a distribution topology, not a time trigger. An SNS topic is reactive by definition: it distributes what was published to it, at the moment it was published. No topic attribute, subscription attribute or delivery policy introduces time. The question of who publishes on Saturday remains unanswered, so sending stays manual and the original problem does not move. That is an architecture failure in its most direct form, a component chosen that does not address the requirement, and in the vocabulary of the pillars it lands on Operational Excellence, because the manual step stays on the critical path.&lt;/p&gt;

&lt;p&gt;One pillar, tied to one mechanism, is the whole analysis here. Reaching for more would be padding.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Step Functions state machine on a schedule
&lt;/h3&gt;

&lt;p&gt;Before any pillar discussion, the premise has to come apart: AWS Step Functions has no scheduler of its own. The official documentation on starting a state machine execution lists the ways it can happen, and none of them is a scheduler internal to the service: the Step Functions console, a &lt;code&gt;StartExecution&lt;/code&gt; API call, Amazon EventBridge reacting to an event, Amazon EventBridge Scheduler for a time-based start, a nested execution from a &lt;code&gt;Task&lt;/code&gt; state, and Amazon API Gateway. The service does have wait states, which delay an execution that already started, and that is not scheduling. Step Functions even publishes a page of its own on using EventBridge Scheduler to start a state machine execution at a given time. A pillar does not apply to a capability that does not exist, so this is a factual error, not a design trade-off.&lt;/p&gt;

&lt;p&gt;Once the premise is fixed, the design behind the proposal becomes assessable: EventBridge or EventBridge Scheduler triggering a state machine that publishes to SNS. There, pillars apply. A single-step state machine adds a workflow definition, an execution role, an execution history and per-state-transition billing to the path, to do what EventBridge already does by publishing straight to the topic. That is a Cost Optimization failure, since it inserts a component billed per state transition where the native integration bills nothing for the hop, and an Operational Excellence failure, since it adds one more versioned artifact, one more role and one more console to operate without adding a single business step.&lt;/p&gt;

&lt;p&gt;The threshold where that changes is worth naming. Step Functions pays for itself when there is real orchestration: branching per audience segment, waiting on human approval, per-step error handling, parallel calls, retry policies that differ by step. Publishing one message is not orchestration.&lt;/p&gt;

&lt;h3&gt;
  
  
  An EventBridge rule publishing straight to the topic
&lt;/h3&gt;

&lt;p&gt;EventBridge accepts an SNS topic as the target of a rule and performs the publish itself, authorized by the topic resource policy. There is no intermediate function, no code, and no runtime to version. It wins on Operational Excellence, because schedule and authorization are declared in CloudFormation, versioned in Git, and the change history becomes the repository history, with native metrics in the &lt;code&gt;AWS/Events&lt;/code&gt; namespace instead of whatever a script chose to log. It wins on Cost Optimization, because no compute resource participates in the scheduling at all, which leaves SNS delivery as the dominant cost. And it wins on Reliability against the other three designs, because there is no host on the path, with a retry policy and a dead letter queue configurable on the target itself.&lt;/p&gt;

&lt;p&gt;It is not perfect, and that belongs in the post as much as the win does.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Delivery is at-least-once. For a marketing campaign that means a risk of duplicate email, which is the original complaint. The design lowers the probability without eliminating it.&lt;/li&gt;
&lt;li&gt;The cron expression of an &lt;code&gt;AWS::Events::Rule&lt;/code&gt; is interpreted in UTC only, with no time zone property. That is where the wrong-hour mistakes come from, and it is the reason a second template exists further down.&lt;/li&gt;
&lt;li&gt;The payload arrives in a standard event format. An input transformer fixes that, and the objection is legitimate enough to deserve the mention.&lt;/li&gt;
&lt;li&gt;There is no segmentation, no per-recipient content template and no delivery report. Those limits are the subject of the trade-offs section.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing the smallest operational surface
&lt;/h2&gt;

&lt;p&gt;I chose the scheduled rule on the criterion of smallest operational surface: it is the only one of the four designs where nothing new has to be maintained after the deploy. No operating system, no runtime dependency, no build artifact. What remains to operate is a set of alarms and an empty dead letter queue.&lt;/p&gt;

&lt;p&gt;I delivered two templates. The first uses &lt;code&gt;AWS::Events::Rule&lt;/code&gt;, which is the classic answer and the one the team recognized. The second uses &lt;code&gt;AWS::Scheduler::Schedule&lt;/code&gt;, and it is the one I would leave running, for the reason developed further down. The two stacks are alternatives, not complements, and they deploy under different stack names.&lt;/p&gt;

&lt;h2&gt;
  
  
  The publish path, from schedule to subscriber
&lt;/h2&gt;

&lt;p&gt;EventBridge evaluates the schedule expression and, at the scheduled time, calls &lt;code&gt;sns:Publish&lt;/code&gt; on the campaign topic. The topic distributes to the confirmed subscriptions, each protocol at its own pace. If the target invocation fails permanently, the message goes to an SQS dead letter queue with attributes identifying the rule, the target and the error. Four CloudWatch alarms in the rule template watch the places where the flow can break quietly: invocation failure on the EventBridge side, a denied write to the dead letter queue, dead letter queue depth, and delivery failure on the SNS side. The Scheduler template has five, because its trigger side reports the target error and the dropped invocation as separate metrics.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
  RULE[EventBridge scheduled rule] --&amp;gt;|sns:Publish| TOPIC[SNS topic encrypted with KMS]
  TOPIC --&amp;gt; MAIL[Email subscriptions]
  TOPIC --&amp;gt; SMS[SMS subscriptions]
  TOPIC --&amp;gt; PUSH[Mobile push subscriptions]
  TOPIC --&amp;gt; QUEUE[SQS queue for the internal consumer]
  RULE --&amp;gt;|retries exhausted| DLQ[(SQS dead letter queue)]
  RULE -.-&amp;gt;|FailedInvocations| CW[CloudWatch alarms]
  RULE -.-&amp;gt;|InvocationsFailedToBeSentToDlq| CW
  DLQ -.-&amp;gt;|ApproximateNumberOfMessagesVisible| CW
  TOPIC -.-&amp;gt;|NumberOfNotificationsFailed| CW&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  EventBridge cron expressions run in UTC
&lt;/h2&gt;

&lt;p&gt;This detail cost a campaign at an earlier client, so it comes before the template.&lt;/p&gt;

&lt;p&gt;The cron expression of an &lt;code&gt;AWS::Events::Rule&lt;/code&gt; has six fields and is always evaluated in UTC. There is no time zone property on the resource. The AWS documentation makes it explicit by annotating every example hour with UTC+0.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Wildcards accepted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;0-59&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;td&gt;0-23&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day-of-month&lt;/td&gt;
&lt;td&gt;1-31&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;/&lt;/code&gt; &lt;code&gt;L&lt;/code&gt; &lt;code&gt;W&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Month&lt;/td&gt;
&lt;td&gt;1-12 or JAN-DEC&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Day-of-week&lt;/td&gt;
&lt;td&gt;1-7 or SUN-SAT&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;L&lt;/code&gt; &lt;code&gt;#&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Year&lt;/td&gt;
&lt;td&gt;1970-2199&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;,&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The sixth field, year, is not optional. Anyone pasting a five-field crontab gets a validation error.&lt;/p&gt;

&lt;p&gt;The second trap sits in the day fields. The documentation does not allow day-of-month and day-of-week in the same expression: if one of them carries a value or &lt;code&gt;*&lt;/code&gt;, the other needs &lt;code&gt;?&lt;/code&gt;. That is why the campaign expression is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cron(0 12 ? * SAT *)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and not &lt;code&gt;cron(0 12 * * SAT *)&lt;/code&gt;, which is rejected for using &lt;code&gt;*&lt;/code&gt; in day-of-month alongside &lt;code&gt;SAT&lt;/code&gt; in day-of-week. The &lt;code&gt;?&lt;/code&gt; means any, and it marks the field as not participating in the decision.&lt;/p&gt;

&lt;p&gt;On the hour itself: 12:00 UTC corresponds to 09:00 in &lt;code&gt;America/Sao_Paulo&lt;/code&gt;, which sits at UTC-3. For this origin zone the risk is not that the offset moves. It is that the conversion exists at all and lives in the operator's head, so the number in the template no longer says what the business asked for, and reading it back correctly depends on someone remembering the offset. Add the subscribers in other countries, where the offset does shift twice a year, and the rule keeps firing at the same absolute instant while the local hour moves under it. Both failures come from the same place: the expression carries no time zone, so the time zone has to be carried by a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CloudFormation template for the EventBridge rule
&lt;/h2&gt;

&lt;p&gt;Full file in &lt;a href="//iac/eventbridge-sns-notifications.yaml"&gt;&lt;code&gt;iac/eventbridge-sns-notifications.yaml&lt;/code&gt;&lt;/a&gt;. What follows are the parts that need justification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parameter validation that fails the deploy instead of the campaign
&lt;/h3&gt;

&lt;p&gt;Everything that changes between accounts is a parameter, with validation. Three of them carry most of the information. The topic ARN decides whether the stack owns the topic at all and gets a section of its own below. The other two are the key and the schedule expression:&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="na"&gt;ExistingKmsKeyArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;AllowedPattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^$|^arn:aws[a-zA-Z-]*:kms:[a-z0-9-]+:[0-9]{12}:key/[0-9a-zA-Z-]+$'&lt;/span&gt;
    &lt;span class="na"&gt;ConstraintDescription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;Leave empty or provide the ARN of a KMS key, in the form&lt;/span&gt;
      &lt;span class="s"&gt;arn:aws:kms:REGION:ACCOUNT:key/KEY_ID. An alias ARN is rejected on&lt;/span&gt;
      &lt;span class="s"&gt;purpose: alias/aws/sns has a key policy that cannot be edited, so the&lt;/span&gt;
      &lt;span class="s"&gt;grant to events.amazonaws.com has nowhere to go.&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;ARN of an existing customer managed KMS key for the topic this stack&lt;/span&gt;
      &lt;span class="s"&gt;creates. Leave empty and the stack creates its own key. Ignored when&lt;/span&gt;
      &lt;span class="s"&gt;ExistingTopicArn is set, because that topic keeps whatever key it already&lt;/span&gt;
      &lt;span class="s"&gt;has. The key you name here has to grant kms:GenerateDataKey* and&lt;/span&gt;
      &lt;span class="s"&gt;kms:Decrypt to events.amazonaws.com in its own key policy: this stack&lt;/span&gt;
      &lt;span class="s"&gt;writes a key policy only for the key it creates. Do not point this at&lt;/span&gt;
      &lt;span class="s"&gt;alias/aws/sns either, because the key policy of an AWS managed key cannot&lt;/span&gt;
      &lt;span class="s"&gt;be edited, so the grant has nowhere to go and the rule publish fails with&lt;/span&gt;
      &lt;span class="s"&gt;KMSAccessDeniedException.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AllowedPattern&lt;/code&gt; on that parameter is the part doing real work. The warning in the &lt;code&gt;Description&lt;/code&gt; is readable, and a warning in a description stops nobody: pass an alias ARN and the stack reaches &lt;code&gt;CREATE_COMPLETE&lt;/code&gt;, then the campaign fails on the first Saturday. The pattern accepts an empty string or a key ARN and rejects anything with &lt;code&gt;alias/&lt;/code&gt; in the resource part, which turns the warning into a parameter validation failure at deploy time. The same parameter in the Scheduler template carries the pattern for a second reason: there the value also lands in the &lt;code&gt;Resource&lt;/code&gt; of an IAM statement, and a malformed ARN there fails the role creation with &lt;code&gt;MalformedPolicyDocument&lt;/code&gt;, an error that names neither the parameter nor the template line that caused it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AlarmTopicArn&lt;/code&gt; gets the same treatment, accepting an empty string or an SNS topic ARN. That one is a partial guard and worth saying so: the ARN of the campaign topic is itself a valid SNS topic ARN, so the pattern catches a typo and does not catch the mistake that matters, which is pointing the alarm action at the campaign topic.&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="na"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cron(0 12 ? * SAT *)&lt;/span&gt;
    &lt;span class="na"&gt;MinLength&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;Schedule expression of the rule. AWS::Events::Rule always evaluates cron&lt;/span&gt;
      &lt;span class="s"&gt;in UTC, with no time zone option. cron(0 12 ? * SAT *) is Saturday 12:00&lt;/span&gt;
      &lt;span class="s"&gt;UTC, which is 09:00 in America/Sao_Paulo. The ? in the day-of-month field&lt;/span&gt;
      &lt;span class="s"&gt;is required whenever the day-of-week field carries a value.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Whether the stack owns the campaign topic
&lt;/h3&gt;

&lt;p&gt;The requirement was that the existing topic keeps its subscriptions. The first version of this template declared an &lt;code&gt;AWS::SNS::Topic&lt;/code&gt; and took the name as a parameter, which looks like it honors that requirement and does the opposite, for one reason: &lt;code&gt;sns:CreateTopic&lt;/code&gt; is idempotent. The API reference states it plainly, that a requester who already owns a topic with the given name gets that topic's ARN back without a new topic being created.&lt;/p&gt;

&lt;p&gt;Follow what CloudFormation does with that. It calls &lt;code&gt;CreateTopic&lt;/code&gt; with the name from the parameter. The production topic already exists under that name, so the call succeeds and returns its ARN, and the stack reaches &lt;code&gt;CREATE_COMPLETE&lt;/code&gt; holding the production topic as the physical resource of a logical resource it believes it created. Then: the topic policy written by the stack replaces the policy of that topic, so every publisher that depended on the previous one loses access; &lt;code&gt;DisplayName&lt;/code&gt; and &lt;code&gt;KmsMasterKeyId&lt;/code&gt; overwrite what was there, which changes the sender name on email and the prefix on SMS for the current subscribers; and &lt;code&gt;delete-stack&lt;/code&gt; deletes the topic along with the confirmed subscriptions. Nothing in that sequence produces an error. A loud &lt;code&gt;CREATE_FAILED&lt;/code&gt; would have been the safe outcome, and it is not what happens.&lt;/p&gt;

&lt;p&gt;So ownership is a parameter, not an accident:&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="na"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
    &lt;span class="na"&gt;AllowedPattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;^$|^arn:aws[a-zA-Z-]*:sns:[a-z0-9-]+:[0-9]{12}:[a-zA-Z0-9_-]{1,256}$'&lt;/span&gt;
    &lt;span class="na"&gt;ConstraintDescription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;Leave empty or provide the ARN of an SNS topic, in the form&lt;/span&gt;
      &lt;span class="s"&gt;arn:aws:sns:REGION:ACCOUNT:TOPIC_NAME. The pattern accepts a standard&lt;/span&gt;
      &lt;span class="s"&gt;topic only. The ARN of a FIFO topic ends in .fifo and is rejected on&lt;/span&gt;
      &lt;span class="s"&gt;purpose, because a FIFO topic is limited to 100 subscriptions and cannot&lt;/span&gt;
      &lt;span class="s"&gt;carry this campaign.&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;-&lt;/span&gt;
      &lt;span class="s"&gt;ARN of the campaign topic that already carries the production&lt;/span&gt;
      &lt;span class="s"&gt;subscriptions. Leave empty and the stack creates its own topic, which is&lt;/span&gt;
      &lt;span class="s"&gt;the self-contained lab. With a value, the stack creates no topic and does&lt;/span&gt;
      &lt;span class="s"&gt;not delete one on teardown, and TopicName, TopicDisplayName and&lt;/span&gt;
      &lt;span class="s"&gt;ExistingKmsKeyArn are ignored. TestSubscriptionEmail still applies and&lt;/span&gt;
      &lt;span class="s"&gt;creates a real subscription on the topic you name, so leave it empty here&lt;/span&gt;
      &lt;span class="s"&gt;unless you want that address on the live campaign. Two prerequisites on&lt;/span&gt;
      &lt;span class="s"&gt;the topic you name: this stack replaces its access policy, so any&lt;/span&gt;
      &lt;span class="s"&gt;publisher that depended on the previous policy loses access, and if the&lt;/span&gt;
      &lt;span class="s"&gt;topic is encrypted its key&lt;/span&gt;
      &lt;span class="s"&gt;policy already has to grant kms:GenerateDataKey* and kms:Decrypt to&lt;/span&gt;
      &lt;span class="s"&gt;events.amazonaws.com, because this stack does not write to a key it did&lt;/span&gt;
      &lt;span class="s"&gt;not create.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;CreateTopic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Equals&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="c1"&gt;# The stack only writes a key policy for a key it creates, and it only has a&lt;/span&gt;
  &lt;span class="c1"&gt;# topic to attach a key to when it creates the topic. Pointing at an existing&lt;/span&gt;
  &lt;span class="c1"&gt;# topic therefore creates no key, instead of leaving an orphan key behind.&lt;/span&gt;
  &lt;span class="na"&gt;CreateKmsKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!And&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Condition&lt;/span&gt; &lt;span class="s"&gt;CreateTopic&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Equals&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingKmsKeyArn&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Left empty, the stack is a self-contained lab: it stands up a topic equivalent to the production one, wires the schedule to it, and the teardown takes it back out. That is the mode to use while you are still shaping the stack.&lt;/p&gt;

&lt;p&gt;With an ARN in it, the topic is outside the stack, which is the point. There is no &lt;code&gt;AWS::SNS::Topic&lt;/code&gt; to adopt and no physical resource for &lt;code&gt;delete-stack&lt;/code&gt; to remove, so the destructive path closes. The ARN then flows to the &lt;code&gt;Topics&lt;/code&gt; and &lt;code&gt;Resource&lt;/code&gt; of the topic policy, the rule target, the optional test subscription, the &lt;code&gt;TopicName&lt;/code&gt; dimension of the SNS delivery alarm, and the &lt;code&gt;TopicArn&lt;/code&gt; output. The alarm dimension is the one that needed thought: it takes a name, not an ARN, and &lt;code&gt;!GetAtt CampaignTopic.TopicName&lt;/code&gt; does not exist on this branch, so it comes out of the ARN:&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="na"&gt;Dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# The AWS/SNS dimension is the topic name, which has to come out of the&lt;/span&gt;
        &lt;span class="c1"&gt;# ARN when the topic was not created here.&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;TopicName&lt;/span&gt;
          &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!If&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CreateTopic&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignTopic.TopicName&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Select&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;5&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Split&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;:'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing the parameter does not make safe, and it has to be said rather than buried in a description: pointing at a production topic still replaces that topic's access policy, because &lt;code&gt;AWS::SNS::TopicPolicy&lt;/code&gt; replaces instead of adding. Read the existing policy first, with &lt;code&gt;aws sns get-topic-attributes&lt;/code&gt;, and carry anything you still need into this template. The parameter removes the deletion path, not the authorization path.&lt;/p&gt;

&lt;p&gt;The test subscription deserves the same warning. It is not ignored in this mode, so an address left in &lt;code&gt;TestSubscriptionEmail&lt;/code&gt; becomes a real subscription on the production topic, gets a confirmation mail, and from then on receives the actual campaign alongside everyone else. Leave the parameter empty once the topic is the live one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption at rest, and why the AWS managed key does not work here
&lt;/h3&gt;

&lt;p&gt;My baseline for an SNS topic is encryption at rest with the AWS managed key, &lt;code&gt;alias/aws/sns&lt;/code&gt;, with a parameter left open for a customer managed key when compliance asks for one. In this design that baseline does not work, and I found out on the first deploy attempt.&lt;/p&gt;

&lt;p&gt;The SNS documentation is direct about it: for an AWS service to publish to an encrypted topic, you need a customer managed key, and you have to grant &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; to the service principal in the key policy. The table of principals lists &lt;code&gt;events.amazonaws.com&lt;/code&gt;. The key policy of an AWS managed key cannot be edited, so there is nowhere to put that grant. With &lt;code&gt;alias/aws/sns&lt;/code&gt; on the topic, the rule fires, the publish fails, and the error is &lt;code&gt;KMSAccessDeniedException&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is a second part to it, and it runs against habit: the same page records that &lt;code&gt;aws:SourceAccount&lt;/code&gt;, &lt;code&gt;aws:SourceArn&lt;/code&gt; and &lt;code&gt;aws:SourceOrgID&lt;/code&gt; are not supported in a key policy on the EventBridge to encrypted topic path. The KMS statement stays without an origin condition. That does not open the topic, because who may publish is still controlled by the topic policy, but it is an exception to the reflex of conditioning every service grant.&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="na"&gt;CampaignKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::KMS::Key&lt;/span&gt;
    &lt;span class="na"&gt;Condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CreateKmsKey&lt;/span&gt;
    &lt;span class="na"&gt;DeletionPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;
    &lt;span class="na"&gt;UpdateReplacePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Retain&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Encryption&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;at&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rest&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;campaign&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;topic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;stack&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}'&lt;/span&gt;
      &lt;span class="na"&gt;EnableKeyRotation&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;KeyPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AllowAccountIamPolicies&lt;/span&gt;
            &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;AWS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:${AWS::Partition}:iam::${AWS::AccountId}:root'&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kms:*'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
          &lt;span class="c1"&gt;# No Condition here on purpose: the SNS documentation records that&lt;/span&gt;
          &lt;span class="c1"&gt;# aws:SourceArn, aws:SourceAccount and aws:SourceOrgID are not&lt;/span&gt;
          &lt;span class="c1"&gt;# supported in a key policy on the EventBridge to encrypted topic&lt;/span&gt;
          &lt;span class="c1"&gt;# path. Who may publish stays controlled by the topic policy below.&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AllowEventBridgeToEncryptMessages&lt;/span&gt;
            &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kms:GenerateDataKey*'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;kms:Decrypt'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That grant is in the key policy of the key this stack creates, which means it covers one of the ways the parameter can be used and not the other. Pass &lt;code&gt;ExistingKmsKeyArn&lt;/code&gt; and no key policy is written by the stack at all: the topic uses a key whose policy the template does not control and cannot read. The &lt;code&gt;AllowedPattern&lt;/code&gt; rejects an alias ARN and says nothing about the contents of a key policy, so a customer managed key without the grant to &lt;code&gt;events.amazonaws.com&lt;/code&gt; passes validation, the stack reaches &lt;code&gt;CREATE_COMPLETE&lt;/code&gt;, and the first Saturday reproduces the same &lt;code&gt;KMSAccessDeniedException&lt;/code&gt; this section is about. The diagnosis is in the dead letter queue, under &lt;code&gt;ERROR_CODE&lt;/code&gt;, as &lt;code&gt;ACTION_DECRYPTION_FAILURE&lt;/code&gt;. If you supply a key, read its policy before you supply it.&lt;/p&gt;

&lt;p&gt;The key carries &lt;code&gt;DeletionPolicy: Retain&lt;/code&gt; on purpose, and the teardown section says what that means in practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The topic policy, and what the origin conditions prevent
&lt;/h3&gt;

&lt;p&gt;Authorization for EventBridge to publish is a resource policy on the topic, with principal &lt;code&gt;events.amazonaws.com&lt;/code&gt; and action &lt;code&gt;sns:Publish&lt;/code&gt;. The documentation example for this case comes with no condition at all, and a template that copies that example literally grants publish rights to any EventBridge rule in any AWS account. That is the confused deputy problem in its most direct form: the service holds the permission, and your resource does not check on whose behalf the service is acting.&lt;/p&gt;

&lt;p&gt;The EventBridge documentation on the subject says which value to use: for a target of a rule on an event bus, &lt;code&gt;aws:SourceArn&lt;/code&gt; has to be the ARN of the rule. I add &lt;code&gt;aws:SourceAccount&lt;/code&gt; to tie it to this account.&lt;/p&gt;

&lt;p&gt;The policy has one statement, and the decision to keep it at one is the subject of the paragraphs after the block.&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="na"&gt;CampaignTopicPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SNS::TopicPolicy&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Topics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!If&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;CreateTopic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;CampaignTopic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Without the two conditions below, any EventBridge rule in any&lt;/span&gt;
          &lt;span class="c1"&gt;# account could publish here. aws:SourceArn ties the permission to&lt;/span&gt;
          &lt;span class="c1"&gt;# this rule; aws:SourceAccount ties it to this account. The SNS&lt;/span&gt;
          &lt;span class="c1"&gt;# access control documentation lists Amazon EventBridge among the&lt;/span&gt;
          &lt;span class="c1"&gt;# services that set SourceAccount when publishing to a topic, so both&lt;/span&gt;
          &lt;span class="c1"&gt;# keys are in the request context on this path.&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishOnlyFromTheScheduledRule&lt;/span&gt;
            &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sns:Publish'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!If&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;CreateTopic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;CampaignTopic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
            &lt;span class="na"&gt;Condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;ArnEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws:SourceArn'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignRule.Arn&lt;/span&gt;
              &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws:SourceAccount'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;AWS::AccountId&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both condition keys have to be in the request context for that statement to grant anything, and that is worth checking rather than assuming. The SNS access control documentation lists the services that set &lt;code&gt;SourceAccount&lt;/code&gt; when they publish to a topic, and Amazon EventBridge is on the list. The same page records that &lt;code&gt;aws:SourceOwner&lt;/code&gt;, which appears in older examples for the same purpose, is deprecated, and that new services integrate through &lt;code&gt;aws:SourceArn&lt;/code&gt; and &lt;code&gt;aws:SourceAccount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AWS::SNS::TopicPolicy&lt;/code&gt; replaces the topic policy rather than adding to it, which is the fact that makes the single statement a decision instead of an omission. The resulting document is exactly what is written above. There is no administrative statement, no statement for the operations team, and no statement for the account.&lt;/p&gt;

&lt;p&gt;I wrote one in the first version of this template, and then took it out. The reasoning behind writing it goes like this: the policy replaces the default, the default grants the account owner the administrative actions, so the replacement should restore them. Two things are wrong with that chain. The fact in the middle goes first: the SNS documentation describes a new topic as created with no permissions, reachable by the account that created it, which is not a default that hands administration to the account through the topic policy, so there is nothing of that kind to restore. The decisive error is the assumption the conclusion rests on and never states. A resource policy grants access on top of whatever the caller identity policy already allows, not instead of it. An administrative statement with principal &lt;code&gt;*&lt;/code&gt; and a condition tying it to this account grants those actions to every identity in the account, including a role whose identity policy carries no SNS permission at all. With &lt;code&gt;sns:DeleteTopic&lt;/code&gt;, &lt;code&gt;sns:SetTopicAttributes&lt;/code&gt; and &lt;code&gt;sns:Subscribe&lt;/code&gt; in the list, any internal principal could drop the campaign topic, rewrite this policy or attach an endpoint of its choosing to a topic with millions of subscriptions. &lt;code&gt;sns:AddPermission&lt;/code&gt; is the worst of them, because it is the action that adds a cross-account &lt;code&gt;Allow&lt;/code&gt; for &lt;code&gt;sns:Publish&lt;/code&gt;, which is the exact requirement the design was supposed to hold. No principal outside the account can publish today, and that statement let any principal inside the account change it.&lt;/p&gt;

&lt;p&gt;Administration of a topic in the same account needs no statement in the resource policy. The identity policy of the caller authorizes it alone. Manual publishing works the same way, and the validation section comes back to that, because it changes how a failed test publish gets diagnosed.&lt;/p&gt;

&lt;p&gt;There is a second trap in the vicinity, and it is about which condition key belongs on which statement. The IAM global condition key reference describes &lt;code&gt;aws:SourceAccount&lt;/code&gt; as entering the request context when the call to the resource is made by an AWS service principal, carrying the account the service acts on behalf of. It describes &lt;code&gt;aws:PrincipalAccount&lt;/code&gt; as present in every request, anonymous ones included, comparing the account the calling principal belongs to. Two keys with similar names, two different request contexts. The publish statement above is called by a service principal, which is where &lt;code&gt;aws:SourceAccount&lt;/code&gt; applies. A statement written for a call made directly by an IAM principal is a different context and reaches for &lt;code&gt;aws:PrincipalAccount&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;What makes that easy to get backwards is the shape of the policy people copy from. The EventBridge documentation on resource-based policies reproduces a default SNS topic policy whose single statement has &lt;code&gt;Principal&lt;/code&gt; as &lt;code&gt;*&lt;/code&gt; and conditions the administrative actions on &lt;code&gt;AWS:SourceAccount&lt;/code&gt;. Lifting that shape into a hand-written policy is how a statement ends up resting on a key nobody verified for that path. For the case here the SNS access control documentation closes the question from the other side: a new topic is created private, with no permissions, reachable by the account that created it. Administration inside the same account needs no statement on the topic. It needs an identity policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  The input transformer and the execution identifier
&lt;/h3&gt;

&lt;p&gt;The default scheduled event reaches the subscriber as the metadata envelope every EventBridge event carries, with the fields &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;detail-type&lt;/code&gt;, &lt;code&gt;source&lt;/code&gt;, &lt;code&gt;account&lt;/code&gt;, &lt;code&gt;time&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, &lt;code&gt;resources&lt;/code&gt; and &lt;code&gt;detail&lt;/code&gt;. For a scheduled rule the service fills it the way the events reference documents, with the ARN of the rule in &lt;code&gt;resources&lt;/code&gt; and &lt;code&gt;detail&lt;/code&gt; empty, because a firing has nothing to describe:&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;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7c4b21f0-9e3a-4b55-8f21-2c6d9a4f1b8e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scheduled Event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws.events"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"123456789012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-10T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us-east-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;"resources"&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="s2"&gt;"arn:aws:events:us-east-1:123456789012:rule/weekly-campaigns-rule-saturday-trigger"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&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;The &lt;code&gt;time&lt;/code&gt; is the firing this rule produces: Saturday, October 10, 2026, 12:00 UTC, 09:00 in &lt;code&gt;America/Sao_Paulo&lt;/code&gt;. The &lt;code&gt;id&lt;/code&gt; is a UUID that EventBridge generates per event. Useful for debugging, useless as a campaign, since what the subscriber would read is the envelope rather than a message. The input transformer replaces the payload.&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="na"&gt;InputTransformer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;InputPathsMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;eventId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$.id'&lt;/span&gt;
              &lt;span class="na"&gt;scheduledTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$.time'&lt;/span&gt;
            &lt;span class="na"&gt;InputTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
              &lt;span class="s"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"campaign": "weekly-newsletter",&lt;/span&gt;
                &lt;span class="s"&gt;"executionId": "&amp;lt;eventId&amp;gt;",&lt;/span&gt;
                &lt;span class="s"&gt;"scheduledTime": "&amp;lt;scheduledTime&amp;gt;",&lt;/span&gt;
                &lt;span class="s"&gt;"ingestionTime": "&amp;lt;aws.events.event.ingestion-time&amp;gt;",&lt;/span&gt;
                &lt;span class="s"&gt;"rule": "&amp;lt;aws.events.rule-name&amp;gt;"&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;InputPathsMap&lt;/code&gt; declares variables from JSONPath over the original event. &lt;code&gt;aws.events.event.ingestion-time&lt;/code&gt; and &lt;code&gt;aws.events.rule-name&lt;/code&gt; are predefined variables: they do not need declaring, and the first one is generated by EventBridge as an ISO 8601 timestamp and cannot be overwritten. A variable pointing at a scalar value goes inside quotes in the template; a variable pointing at an object or array does not.&lt;/p&gt;

&lt;p&gt;Against the event above, the template renders the body the topic publishes:&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;"campaign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly-newsletter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7c4b21f0-9e3a-4b55-8f21-2c6d9a4f1b8e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheduledTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-10T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ingestionTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-10T12:00:03Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rule"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly-campaigns-rule-saturday-trigger"&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;The two timestamps are not the same instant, and the gap is the design rather than a defect: &lt;code&gt;scheduledTime&lt;/code&gt; is the minute the schedule fired, &lt;code&gt;ingestionTime&lt;/code&gt; is when EventBridge received the event, and the documentation is explicit that several seconds can pass between the scheduled time and the target running. A consumer that treats them as interchangeable will eventually file a firing under the wrong minute.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;executionId&lt;/code&gt; field carries the event &lt;code&gt;id&lt;/code&gt;, and it exists because of the idempotency discussion further down. Retries of the same event keep the same &lt;code&gt;id&lt;/code&gt;, while each weekly firing produces a new one. That is the key a consumer uses to recognize an occurrence it already processed.&lt;/p&gt;

&lt;p&gt;One consequence has to be clear: plain email subscribers receive this JSON as the message body. For a campaign with formatted content, either the &lt;code&gt;InputTemplate&lt;/code&gt; emits a string instead of JSON, or the topic feeds a consumer that renders the content. The JSON envelope serves the design where there is a consumer on the other side.&lt;/p&gt;

&lt;h3&gt;
  
  
  Explicit retry policy and dead letter queue
&lt;/h3&gt;

&lt;p&gt;EventBridge defaults to retrying for 24 hours and up to 185 attempts, with exponential backoff and jitter. For a campaign with a fixed hour, that default has an unwanted effect: a long outage results in delivery the next day, which for marketing is worse than no delivery. I cut the window to one hour and send the rest to the dead letter queue.&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="na"&gt;RetryPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;MaximumEventAgeInSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
            &lt;span class="na"&gt;MaximumRetryAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
          &lt;span class="na"&gt;DeadLetterConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignDlq.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dead letter queue is not decoration. Some errors never go through a retry at all: missing permission on the target, a target that no longer exists, an address resolution failure. The documentation says those go straight to the dead letter queue when one is configured. Without it, the only clue would be the &lt;code&gt;FailedInvocations&lt;/code&gt; metric, which tells you a failure happened and carries neither the payload nor the reason. Each message in the queue arrives with the attributes &lt;code&gt;RULE_ARN&lt;/code&gt;, &lt;code&gt;TARGET_ARN&lt;/code&gt;, &lt;code&gt;ERROR_CODE&lt;/code&gt;, &lt;code&gt;ERROR_MESSAGE&lt;/code&gt;, &lt;code&gt;EXHAUSTED_RETRY_CONDITION&lt;/code&gt; and &lt;code&gt;RETRY_ATTEMPTS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two documented constraints on an EventBridge dead letter queue: standard queues only, FIFO is not accepted, and the queue has to be in the same region as the rule.&lt;/p&gt;

&lt;p&gt;Queue retention is the parameter I nearly left at the default. &lt;code&gt;MessageRetentionPeriod&lt;/code&gt; accepts from 60 seconds to 1,209,600 seconds, which is 14 days, and the default is 345,600 seconds, 4 days. On a weekly campaign, 4 days is short: a Saturday failure expires on Wednesday, before anyone has a reason to look. The template uses the maximum.&lt;/p&gt;

&lt;p&gt;The permission to write to the queue deserves a warning. When you configure a dead letter queue through the console, the console attaches the resource policy to the queue for you. Through the API or CloudFormation, it does not. It is a common and quiet deploy error: the stack comes up, the rule fires, the invocation fails, and the attempt to write to the dead letter queue fails too. The symptom shows up in &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;, and the queue depth alarm says nothing, because in that failure mode nothing ever reaches the queue.&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="na"&gt;CampaignDlqPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SQS::QueuePolicy&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Queues&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CampaignDlq&lt;/span&gt;
      &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="c1"&gt;# Only aws:SourceArn here, matching the target dead letter queue&lt;/span&gt;
          &lt;span class="c1"&gt;# example in the EventBridge documentation. The rule ARN already&lt;/span&gt;
          &lt;span class="c1"&gt;# carries the account ID, so the account pin is not lost, and this&lt;/span&gt;
          &lt;span class="c1"&gt;# path has no published confirmation that aws:SourceAccount reaches&lt;/span&gt;
          &lt;span class="c1"&gt;# the request context. An Allow conditioned on a key that is absent&lt;/span&gt;
          &lt;span class="c1"&gt;# grants nothing, and since AWS::SQS::QueuePolicy replaces the queue&lt;/span&gt;
          &lt;span class="c1"&gt;# policy, that would silently take the dead letter path offline.&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AllowEventBridgeToWriteToTheDlq&lt;/span&gt;
            &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sqs:SendMessage'&lt;/span&gt;
            &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignDlq.Arn&lt;/span&gt;
            &lt;span class="na"&gt;Condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;ArnEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws:SourceArn'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignRule.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That statement carries one condition key, and the topic policy a few paragraphs up carries two. The asymmetry is deliberate. For the publish path the SNS documentation names EventBridge as a service that sets &lt;code&gt;SourceAccount&lt;/code&gt;, so both keys are confirmed present. For a write to a target dead letter queue the official example in the EventBridge dead letter queue documentation uses only &lt;code&gt;aws:SourceArn&lt;/code&gt;, and I did not find published confirmation that &lt;code&gt;aws:SourceAccount&lt;/code&gt; reaches the request context on that path. Adding a key on a guess has an asymmetric cost here. An &lt;code&gt;Allow&lt;/code&gt; whose condition cannot match grants nothing, &lt;code&gt;AWS::SQS::QueuePolicy&lt;/code&gt; replaces the queue policy rather than adding to it, and the result would be the exact quiet failure described above, with the whole evidence trail gone. The rule ARN already contains the account ID, so pinning the account is not something the shorter statement gives up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Four alarms and the TreatMissingData detail
&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;RuleFailedInvocationsAlarm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::CloudWatch::Alarm&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AlarmName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-failed-invocations'&lt;/span&gt;
      &lt;span class="na"&gt;AlarmDescription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;The scheduled rule could not publish to the SNS topic&lt;/span&gt;
      &lt;span class="na"&gt;Namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS/Events&lt;/span&gt;
      &lt;span class="na"&gt;MetricName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FailedInvocations&lt;/span&gt;
      &lt;span class="na"&gt;Dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&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;RuleName&lt;/span&gt;
          &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CampaignRule&lt;/span&gt;
      &lt;span class="na"&gt;Statistic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sum&lt;/span&gt;
      &lt;span class="na"&gt;Period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
      &lt;span class="na"&gt;EvaluationPeriods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;0&lt;/span&gt;
      &lt;span class="na"&gt;ComparisonOperator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreaterThanThreshold&lt;/span&gt;
      &lt;span class="na"&gt;TreatMissingData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notBreaching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;FailedInvocations&lt;/code&gt; is sent to CloudWatch only when it is non-zero. On a healthy weekly rule, the metric spends the whole week without a single data point. With the default &lt;code&gt;TreatMissingData&lt;/code&gt;, the alarm lives in &lt;code&gt;INSUFFICIENT_DATA&lt;/code&gt; and nobody trusts it. &lt;code&gt;notBreaching&lt;/code&gt; reads the absence of data as success, which is the correct semantics for this metric. Every alarm in both templates sets it explicitly for the same reason.&lt;/p&gt;

&lt;p&gt;The second alarm watches &lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt; on the dead letter queue, because a message sitting there is a firing that was lost with evidence attached.&lt;/p&gt;

&lt;p&gt;The third one is there because the first two give the same answer to two different questions. &lt;code&gt;FailedInvocations&lt;/code&gt; fires whether the evidence was preserved or discarded, and the depth alarm stays at zero in the case where the write to the queue was denied, since nothing arrived. The operator ends up with one signal covering both "it failed, go read the queue" and "it failed, there is nothing to read", and that difference decides whether the campaign can be reprocessed. &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;, in &lt;code&gt;AWS/Events&lt;/code&gt; with the &lt;code&gt;RuleName&lt;/code&gt; dimension, separates them:&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="na"&gt;RuleDlqWriteFailureAlarm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::CloudWatch::Alarm&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AlarmName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-dlq-write-failed'&lt;/span&gt;
      &lt;span class="na"&gt;AlarmDescription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;EventBridge could not write a failed invocation to the dead letter queue&lt;/span&gt;
      &lt;span class="na"&gt;Namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS/Events&lt;/span&gt;
      &lt;span class="na"&gt;MetricName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InvocationsFailedToBeSentToDlq&lt;/span&gt;
      &lt;span class="na"&gt;Dimensions&lt;/span&gt;&lt;span class="pi"&gt;:&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;RuleName&lt;/span&gt;
          &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CampaignRule&lt;/span&gt;
      &lt;span class="na"&gt;Statistic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sum&lt;/span&gt;
      &lt;span class="na"&gt;Period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;
      &lt;span class="na"&gt;EvaluationPeriods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&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;0&lt;/span&gt;
      &lt;span class="na"&gt;ComparisonOperator&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GreaterThanThreshold&lt;/span&gt;
      &lt;span class="na"&gt;TreatMissingData&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;notBreaching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the capitalization. A metric name is case sensitive, the spelling in the metric table of the EventBridge monitoring page is &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;, and prose elsewhere tends to write &lt;code&gt;DLQ&lt;/code&gt; in capitals. An alarm built on the wrong spelling never leaves &lt;code&gt;INSUFFICIENT_DATA&lt;/code&gt; and nothing tells you why.&lt;/p&gt;

&lt;p&gt;The fourth one exists because the first three only see the trigger. A successful publish with delivery failing at scale lights up none of them, and that was the blind spot I wanted closed. &lt;code&gt;NumberOfNotificationsFailed&lt;/code&gt; in the &lt;code&gt;AWS/SNS&lt;/code&gt; namespace, with dimension &lt;code&gt;TopicName&lt;/code&gt;, covers the delivery side. Its threshold is a parameter, &lt;code&gt;DeliveryFailureThreshold&lt;/code&gt;, rather than a hardcoded zero, because the metric counts differently per protocol: it increments by 1 per message when SNS gives up on SQS, email, SMS and push, but counts every failed attempt on HTTP and HTTPS, retries included. On a topic with HTTP or HTTPS subscriptions, a transient endpoint problem produces data points with no definitive loss, and a threshold of zero would page someone for nothing. The ceiling for a single undeliverable message is the &lt;code&gt;numRetries&lt;/code&gt; of the delivery policy on that subscription, which defaults to 3 and accepts up to 100, so the threshold is a value you derive from the retry counts on the topic rather than pick by feel. The trade-offs section works through those policies per endpoint type.&lt;/p&gt;

&lt;p&gt;One more parameter with a reason behind it: &lt;code&gt;AlarmTopicArn&lt;/code&gt;. Pointing the alarm action at the campaign topic would turn every alarm into a notification to millions of subscribers. The alarm topic is an operations topic and has to be a different one. Left empty, the alarms are created with no action, which is what you want while the stack is still being shaped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying the stack
&lt;/h2&gt;

&lt;p&gt;Example values used from here on: &lt;code&gt;&amp;lt;STACK_NAME&amp;gt;&lt;/code&gt; is &lt;code&gt;weekly-campaigns-rule&lt;/code&gt;, &lt;code&gt;&amp;lt;SCHEDULER_STACK_NAME&amp;gt;&lt;/code&gt; is &lt;code&gt;weekly-campaigns-scheduler&lt;/code&gt;, &lt;code&gt;&amp;lt;REGION&amp;gt;&lt;/code&gt; is &lt;code&gt;sa-east-1&lt;/code&gt;, &lt;code&gt;&amp;lt;ACCOUNT_ID&amp;gt;&lt;/code&gt; is &lt;code&gt;123456789012&lt;/code&gt;, the account ID the AWS documentation uses in its own examples and the one every ARN below carries in place of a real account. Two more placeholders appear in the metric read later on, &lt;code&gt;&amp;lt;START_TIME&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;END_TIME&amp;gt;&lt;/code&gt;, both in the format &lt;code&gt;2026-10-08T00:00:00Z&lt;/code&gt;, and they are not fixed values: they bound the window of the test you just ran, not the date of this post. The two stack names are deliberate: the templates are alternatives, and they compose physical names for the queue, the key alias and the alarms from the stack name, so they are meant to live under distinct names rather than replace each other.&lt;/p&gt;

&lt;p&gt;Those two stack names resolve to the following physical names and ARNs. Having the table in front of you beats resolving a &lt;code&gt;!Sub&lt;/code&gt; in your head while reading a command:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Resource&lt;/th&gt;
&lt;th&gt;Physical name&lt;/th&gt;
&lt;th&gt;ARN&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Campaign topic, rule stack&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaign-subscribers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:sns:sa-east-1:123456789012:weekly-campaign-subscribers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled rule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-rule-saturday-trigger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:events:sa-east-1:123456789012:rule/weekly-campaigns-rule-saturday-trigger&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule dead letter queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-rule-dlq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:sqs:sa-east-1:123456789012:weekly-campaigns-rule-dlq&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule topic key alias&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alias/weekly-campaigns-rule-topic-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:kms:sa-east-1:123456789012:alias/weekly-campaigns-rule-topic-key&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rule invocation failure alarm&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-rule-failed-invocations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:cloudwatch:sa-east-1:123456789012:alarm:weekly-campaigns-rule-failed-invocations&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Campaign topic, Scheduler stack&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaign-subscribers-scheduler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:sns:sa-east-1:123456789012:weekly-campaign-subscribers-scheduler&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-scheduler-group&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:scheduler:sa-east-1:123456789012:schedule-group/weekly-campaigns-scheduler-group&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-scheduler-saturday-trigger&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:scheduler:sa-east-1:123456789012:schedule/weekly-campaigns-scheduler-group/weekly-campaigns-scheduler-saturday-trigger&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schedule dead letter queue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;weekly-campaigns-scheduler-dlq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;arn:aws:sqs:sa-east-1:123456789012:weekly-campaigns-scheduler-dlq&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three of those shapes are where a hand-written ARN goes wrong. A schedule ARN carries the group before the name, &lt;code&gt;schedule/GROUP/NAME&lt;/code&gt;, while the group is a resource of its own, &lt;code&gt;schedule-group/GROUP&lt;/code&gt;, and pinning the wrong one in the trust policy condition is the mistake the Scheduler section comes back to. An alarm ARN separates the name with a colon, &lt;code&gt;alarm:NAME&lt;/code&gt;, not a slash. And a queue has two identifiers rather than one: the ARN above, which is what the policies and the &lt;code&gt;DeadLetterConfig&lt;/code&gt; take, and the URL &lt;code&gt;https://sqs.sa-east-1.amazonaws.com/123456789012/weekly-campaigns-rule-dlq&lt;/code&gt;, which is what &lt;code&gt;receive-message&lt;/code&gt; takes and what the &lt;code&gt;DlqUrl&lt;/code&gt; output returns.&lt;/p&gt;

&lt;p&gt;The Scheduler execution role is the one name absent from that table, and that is deliberate in the template: it declares no &lt;code&gt;RoleName&lt;/code&gt;, so CloudFormation composes the physical name from the stack name, the logical ID and a random suffix, giving something shaped like &lt;code&gt;weekly-campaigns-scheduler-CampaignSchedulerRole-&amp;lt;SUFFIX&amp;gt;&lt;/code&gt;. Naming it would mean &lt;code&gt;CAPABILITY_NAMED_IAM&lt;/code&gt; instead of &lt;code&gt;CAPABILITY_IAM&lt;/code&gt; in every deploy below, and a collision the first time the same stack goes to a second region, because IAM is global. Read the ARN off the &lt;code&gt;SchedulerRoleArn&lt;/code&gt; output rather than reconstructing it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;TopicName&lt;/code&gt; is the one physical name that does not derive from the stack name, and it is a parameter with a fixed default on purpose, because the topic ARN is what the existing subscriptions and the other stacks depend on. That makes it the parameter to check before a deploy, for the reason covered in the section on topic ownership: &lt;code&gt;sns:CreateTopic&lt;/code&gt; is idempotent, so CloudFormation gives you no protection against reusing a name. Deploy the same template twice in the same region under two stack names and the second stack does not fail. It succeeds, adopts the same physical topic, replaces the topic policy the first stack wrote, overwrites the display name and the key, and then deletes that topic on whichever &lt;code&gt;delete-stack&lt;/code&gt; runs first, while the other stack goes on reporting &lt;code&gt;CREATE_COMPLETE&lt;/code&gt; for a resource that is gone.&lt;/p&gt;

&lt;p&gt;The check is a command, not a hope for &lt;code&gt;CREATE_FAILED&lt;/code&gt;. Call &lt;code&gt;aws sns get-topic-attributes&lt;/code&gt; on the ARN the parameter would produce, and let a &lt;code&gt;NotFound&lt;/code&gt; be the green light:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sns get-topic-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; arn:aws:sns:&amp;lt;REGION&amp;gt;:&amp;lt;ACCOUNT_ID&amp;gt;:weekly-campaign-subscribers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result on a free name: &lt;code&gt;NotFound&lt;/code&gt;. The code and the exception class differ, which matters if you script this check: the CLI prints the response code, &lt;code&gt;NotFound&lt;/code&gt;, while the SDKs expose the same error as a class named &lt;code&gt;NotFoundException&lt;/code&gt;. Match on the response code. Anything else means the name is taken, and the choice is then to pick another name, deploy to another region, or pass the ARN in &lt;code&gt;ExistingTopicArn&lt;/code&gt; on purpose. The two templates in this post do not share a topic, and the reason is that their defaults differ, not that CloudFormation would have stopped them.&lt;/p&gt;

&lt;p&gt;Lint first. If it is not installed:&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;cfn-lint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cfn-lint iac/eventbridge-sns-notifications.yaml
cfn-lint iac/eventbridge-scheduler-sns.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: empty output and return code 0 on both files, meaning no findings. Then the service-side check, which requires credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation validate-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-body&lt;/span&gt; file://iac/eventbridge-sns-notifications.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: the service returns the parameter list, the template description and the capabilities it requires. Without credentials configured, this command fails on credentials rather than on the template, which is a different outcome from a template problem and should not be read as one.&lt;/p&gt;

&lt;p&gt;Deploy. &lt;code&gt;--capabilities CAPABILITY_IAM&lt;/code&gt; is required by the Scheduler template, which creates a role, and harmless on this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; iac/eventbridge-sns-notifications.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;STACK_NAME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;TopicName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;weekly-campaign-subscribers &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cron(0 12 ? * SAT *)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;RuleState&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ENABLED &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;TestSubscriptionEmail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ops@example.com &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ProjectTag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;weekly-campaigns &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;STACK_NAME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: &lt;code&gt;TopicArn&lt;/code&gt;, &lt;code&gt;RuleName&lt;/code&gt;, &lt;code&gt;RuleArn&lt;/code&gt;, &lt;code&gt;DlqUrl&lt;/code&gt;, &lt;code&gt;DlqArn&lt;/code&gt; and &lt;code&gt;KmsKeyArn&lt;/code&gt;. The &lt;code&gt;TopicArn&lt;/code&gt; has the form &lt;code&gt;arn:aws:sns:sa-east-1:123456789012:weekly-campaign-subscribers&lt;/code&gt; and is the &lt;code&gt;&amp;lt;TOPIC_ARN&amp;gt;&lt;/code&gt; of the commands below, and &lt;code&gt;DlqUrl&lt;/code&gt; is the &lt;code&gt;&amp;lt;DLQ_URL&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Inspecting what was created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events describe-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; weekly-campaigns-rule-saturday-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;

aws events list-targets-by-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rule&lt;/span&gt; weekly-campaigns-rule-saturday-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: the first confirms &lt;code&gt;ScheduleExpression&lt;/code&gt; and &lt;code&gt;State: ENABLED&lt;/code&gt;. The second is the one I read with more attention, because it shows the target &lt;code&gt;Arn&lt;/code&gt;, the &lt;code&gt;InputTransformer&lt;/code&gt; with the rendered template, and the &lt;code&gt;DeadLetterConfig&lt;/code&gt;. If &lt;code&gt;DeadLetterConfig&lt;/code&gt; is not there, the dead letter queue is not wired, whatever the template says.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving the schedule works without waiting for Saturday
&lt;/h2&gt;

&lt;p&gt;Three checks, in increasing order of isolation.&lt;/p&gt;

&lt;p&gt;The first separates delivery failure from scheduling failure. Publish to the topic by hand and confirm the message reaches the test endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sns publish &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic-arn&lt;/span&gt; &amp;lt;TOPIC_ARN&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s1"&gt;'{"campaign":"weekly-newsletter","executionId":"manual-test-001"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s1"&gt;'Delivery test'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: a &lt;code&gt;MessageId&lt;/code&gt; comes back. Two permissions decide this command, and both matter for diagnosis. The topic policy grants &lt;code&gt;sns:Publish&lt;/code&gt; only to the EventBridge service principal, so an operator publishing by hand is authorized by the IAM policy of their own identity, not by the topic policy. On top of that, the topic is encrypted, so the same identity needs &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; on the topic key. A KMS error here points at the key policy or the caller KMS permissions; an SNS authorization error points at the caller IAM policy.&lt;/p&gt;

&lt;p&gt;The second exercises the schedule itself by shortening the interval. This changes the stack and has to be reverted:&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;# WARNING: temporary change. Revert to cron(0 12 ? * SAT *) after the test.&lt;/span&gt;
aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; iac/eventbridge-sns-notifications.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;STACK_NAME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="nv"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'rate(5 minutes)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: within fifteen minutes there are enough data points for the metric read below. Reverting is a &lt;code&gt;deploy&lt;/code&gt; with the original expression. Leaving &lt;code&gt;rate(5 minutes)&lt;/code&gt; forgotten on a production topic is the kind of accident that turns into a real incident, so the revert is part of the test and not a follow-up.&lt;/p&gt;

&lt;p&gt;The third is the metric read. &lt;code&gt;Invocations&lt;/code&gt; in the &lt;code&gt;AWS/Events&lt;/code&gt; namespace counts the times the target was invoked. Set the window to the minutes you spent on the &lt;code&gt;rate(5 minutes)&lt;/code&gt; test, because a window that misses it returns an empty list for the wrong reason, and an empty list is also what the healthy path returns on the failure metric below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudwatch get-metric-statistics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/Events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; Invocations &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;RuleName,Value&lt;span class="o"&gt;=&lt;/span&gt;weekly-campaigns-rule-saturday-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; &amp;lt;START_TIME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; &amp;lt;END_TIME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 300 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statistics&lt;/span&gt; Sum &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: one data point with &lt;code&gt;Sum: 1&lt;/code&gt; per firing inside the window. Swapping &lt;code&gt;--metric-name&lt;/code&gt; for &lt;code&gt;FailedInvocations&lt;/code&gt;, the expected result is &lt;code&gt;Datapoints: []&lt;/code&gt;, because that metric is published only when it is non-zero. An empty list there is the good outcome.&lt;/p&gt;

&lt;p&gt;If something did fail, the dead letter queue holds the detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs receive-message &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &amp;lt;DLQ_URL&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--message-attribute-names&lt;/span&gt; All &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-number-of-messages&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: the &lt;code&gt;ERROR_CODE&lt;/code&gt; attribute directs the diagnosis. &lt;code&gt;NO_PERMISSIONS&lt;/code&gt; points at the topic policy or the key policy. &lt;code&gt;ACTION_DECRYPTION_FAILURE&lt;/code&gt; points at KMS.&lt;/p&gt;

&lt;p&gt;Teardown, which is never optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation delete-stack &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;STACK_NAME&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
aws cloudformation &lt;span class="nb"&gt;wait &lt;/span&gt;stack-delete-complete &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;STACK_NAME&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes the topic only when the stack created it. With &lt;code&gt;ExistingTopicArn&lt;/code&gt; set there is no topic in the stack, so the teardown leaves the topic and the subscriptions it already had in place and takes away the rule, the queue, the alarms and the topic policy. One subscription does go, if &lt;code&gt;TestSubscriptionEmail&lt;/code&gt; was filled in on that deploy: the stack owns the test subscription even when it does not own the topic, and that is the only subscription of an external topic the teardown touches. The policy is the part to plan for: deleting the stack removes the statement that authorized the rule, and it does not restore whatever policy the topic had before the stack replaced it.&lt;/p&gt;

&lt;p&gt;The KMS key carries &lt;code&gt;DeletionPolicy: Retain&lt;/code&gt;, so it survives the &lt;code&gt;delete-stack&lt;/code&gt; and has to be scheduled for deletion separately. That is deliberate: a key deleted along with the stack makes anything encrypted under it unreadable. The alias does not survive, which is what lets you recreate the stack under the same name without a collision.&lt;/p&gt;

&lt;p&gt;The same thing happens on an update, and it surprises people, so it is worth tracing. Supply &lt;code&gt;ExistingKmsKeyArn&lt;/code&gt; or &lt;code&gt;ExistingTopicArn&lt;/code&gt; to a stack that was created with the defaults and &lt;code&gt;CreateKmsKey&lt;/code&gt; turns false, which takes both the key and its alias out of the stack. The key is retained and keeps being billed as a key, with rotation still enabled; the alias has no &lt;code&gt;DeletionPolicy&lt;/code&gt; and is deleted. So the key you have to clean up afterwards no longer has a name pointing at it, and &lt;code&gt;aws kms schedule-key-deletion&lt;/code&gt; needs the key ID. Read it off the &lt;code&gt;KmsKeyArn&lt;/code&gt; output before the update, not after.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ExistingTopicArn&lt;/code&gt; takes a second resource out of the stack on that same update, and this one is the intended path rather than an accident. Passing it turns &lt;code&gt;CreateTopic&lt;/code&gt; false, and &lt;code&gt;CampaignTopic&lt;/code&gt; carries no &lt;code&gt;DeletionPolicy&lt;/code&gt;, so the topic the stack had created is deleted, taking its subscriptions with it. That is the transition from the self-contained lab to the production topic, and discarding the lab topic is the cleanup you want at that moment. Clear &lt;code&gt;TestSubscriptionEmail&lt;/code&gt; in the same update, though. Left filled, the test subscription is not deleted but recreated against the ARN you just supplied, which is the one way these stacks subscribe an address to the production topic. If you would rather keep the lab topic after the switch, &lt;code&gt;DeletionPolicy: Retain&lt;/code&gt; on &lt;code&gt;CampaignTopic&lt;/code&gt; is the edit, and then that topic also survives the teardown and joins the KMS key on the list of things to clean up by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The EventBridge Scheduler version, with a native time zone
&lt;/h2&gt;

&lt;p&gt;The EventBridge documentation classifies scheduled rules as a legacy feature and recommends EventBridge Scheduler for invoking targets on a schedule. For this case the difference that matters is one property: &lt;code&gt;ScheduleExpressionTimezone&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Full file in &lt;a href="//iac/eventbridge-scheduler-sns.yaml"&gt;&lt;code&gt;iac/eventbridge-scheduler-sns.yaml&lt;/code&gt;&lt;/a&gt;.&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="na"&gt;CampaignSchedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Scheduler::Schedule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${AWS::StackName}-saturday-trigger'&lt;/span&gt;
      &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Weekly Saturday campaign, evaluated in the local time zone&lt;/span&gt;
      &lt;span class="na"&gt;GroupName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CampaignScheduleGroup&lt;/span&gt;
      &lt;span class="na"&gt;State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;ScheduleState&lt;/span&gt;
      &lt;span class="na"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;ScheduleExpression&lt;/span&gt;
      &lt;span class="na"&gt;ScheduleExpressionTimezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;ScheduleTimezone&lt;/span&gt;
      &lt;span class="na"&gt;FlexibleTimeWindow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!If&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;UseFlexibleWindow&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FLEXIBLE&lt;/span&gt;
          &lt;span class="na"&gt;MaximumWindowInMinutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;FlexibleWindowMinutes&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;OFF'&lt;/span&gt;
      &lt;span class="na"&gt;Target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Templated Scheduler target for Amazon SNS Publish: the topic ARN is&lt;/span&gt;
        &lt;span class="c1"&gt;# enough, with no service specific parameter block.&lt;/span&gt;
        &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!If&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;CreateTopic&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="nv"&gt;CampaignTopic.TopicArn&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="nv"&gt;ExistingTopicArn&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
        &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignSchedulerRole.Arn&lt;/span&gt;
        &lt;span class="c1"&gt;# Scheduler context attributes. execution-id is assigned per invocation&lt;/span&gt;
        &lt;span class="c1"&gt;# attempt, so it does not work as a deduplication key: whoever&lt;/span&gt;
        &lt;span class="c1"&gt;# deduplicates uses scheduled-time, which is stable across the attempts&lt;/span&gt;
        &lt;span class="c1"&gt;# of one occurrence. An attempt-number above 1 identifies a retry.&lt;/span&gt;
        &lt;span class="na"&gt;Input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"campaign": "weekly-newsletter",&lt;/span&gt;
            &lt;span class="s"&gt;"executionId": "&amp;lt;aws.scheduler.execution-id&amp;gt;",&lt;/span&gt;
            &lt;span class="s"&gt;"attempt": "&amp;lt;aws.scheduler.attempt-number&amp;gt;",&lt;/span&gt;
            &lt;span class="s"&gt;"scheduledTime": "&amp;lt;aws.scheduler.scheduled-time&amp;gt;",&lt;/span&gt;
            &lt;span class="s"&gt;"schedule": "&amp;lt;aws.scheduler.schedule-arn&amp;gt;"&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;RetryPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;MaximumEventAgeInSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
          &lt;span class="na"&gt;MaximumRetryAttempts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
        &lt;span class="na"&gt;DeadLetterConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CampaignDlq.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expression becomes &lt;code&gt;cron(0 9 ? * SAT *)&lt;/code&gt; with &lt;code&gt;ScheduleExpressionTimezone: America/Sao_Paulo&lt;/code&gt;, and the 9 means nine in the morning local time. The conversion leaves the operator's head and becomes the service's responsibility. For a client firing campaigns in several countries, that means one schedule per time zone instead of a conversion spreadsheet.&lt;/p&gt;

&lt;p&gt;The same Saturday firing, with the context attributes resolved by Scheduler instead of by an input transformer, reaches the topic like this:&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;"campaign"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly-newsletter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c7a4k91fe2db5a08"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attempt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"scheduledTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-10T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"schedule"&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:scheduler:sa-east-1:123456789012:schedule/weekly-campaigns-scheduler-group/weekly-campaigns-scheduler-saturday-trigger"&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;Two details in there are worth more than the shape. The &lt;code&gt;executionId&lt;/code&gt; is not a UUID: the documented example of &lt;code&gt;aws.scheduler.execution-id&lt;/code&gt; is a short alphanumeric string, assigned per invocation attempt, which is the second reason the idempotency section refuses it as a deduplication key. And &lt;code&gt;scheduledTime&lt;/code&gt; is written as the UTC instant, which is the documented example format for the attribute, with the &lt;code&gt;Z&lt;/code&gt; suffix, rather than something I read off a capture. The schedule fires at 09:00 local and that is the same moment as 12:00 UTC, so a consumer that formats the field for a human has to apply the time zone back. If your pipeline branches on that value, confirm it on your own schedule before trusting either reading.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Mode: 'OFF'&lt;/code&gt; needs the quotes in YAML, otherwise the parser reads &lt;code&gt;OFF&lt;/code&gt; as boolean false. &lt;code&gt;MaximumWindowInMinutes&lt;/code&gt; accepts 1 to 1440. I left &lt;code&gt;FLEXIBLE&lt;/code&gt; as the default with 5 minutes, because spreading the publish inside a window smooths the spike on the consumer side. A campaign with a contractual cut-off time uses &lt;code&gt;OFF&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three structural differences between the two templates.&lt;/p&gt;

&lt;p&gt;Authorization moves. The rule publishes as a service principal, authorized by the topic resource policy. Scheduler assumes a role and publishes as that role. That takes the KMS complication out of the path: the key policy can delegate to IAM in the ordinary way, and the grant of &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; goes in the role policy with the key ARN as the resource. The same role gets &lt;code&gt;sqs:SendMessage&lt;/code&gt; on the dead letter queue, which is why that template has no &lt;code&gt;AWS::SQS::QueuePolicy&lt;/code&gt;. One permission path instead of two.&lt;/p&gt;

&lt;p&gt;That also lifts the constraint that forced a customer managed key in the other template. The SNS documentation puts the role publishing to an encrypted topic under the ordinary case, where the publisher carries &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; in its own identity policy, so &lt;code&gt;alias/aws/sns&lt;/code&gt; would work here and my usual baseline would apply. This template creates a key anyway, for two reasons worth naming because they are choices and not requirements: an editable key policy is what makes the delegation below visible as a statement you can read, and keeping the key parameter identical across the two stacks means the teardown and the retained-key caveat read the same way in both. Swap it for the AWS managed key and the only thing you lose is that discussion.&lt;/p&gt;

&lt;p&gt;The delegation is doing load-bearing work in that sentence, and it only holds for the key the stack creates. KMS evaluates the key policy and the identity policy together, so an IAM grant on a key whose policy neither delegates to the account root nor names this role authorizes nothing. The key this template creates carries the delegation statement; a key supplied through &lt;code&gt;ExistingKmsKeyArn&lt;/code&gt; may not. That is also why supplying an existing topic does not make the key parameter pointless here, unlike in the rule template: with an external encrypted topic, &lt;code&gt;ExistingKmsKeyArn&lt;/code&gt; is how the role gets access to the key that topic already uses, and the statement is dropped from the role policy altogether when there is no key to name.&lt;/p&gt;

&lt;p&gt;The trust policy gains origin conditions. Scheduler supports &lt;code&gt;aws:SourceArn&lt;/code&gt; pointing at the schedule group, and that is why the template creates a dedicated &lt;code&gt;AWS::Scheduler::ScheduleGroup&lt;/code&gt; instead of using the &lt;code&gt;default&lt;/code&gt; group.&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="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scheduler.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sts:AssumeRole'&lt;/span&gt;
            &lt;span class="c1"&gt;# Without these conditions the role can be assumed on behalf of any&lt;/span&gt;
            &lt;span class="c1"&gt;# schedule in the service, including one in another account, which&lt;/span&gt;
            &lt;span class="c1"&gt;# is the confused deputy problem.&lt;/span&gt;
            &lt;span class="na"&gt;Condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws:SourceAccount'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;AWS::AccountId&lt;/span&gt;
              &lt;span class="na"&gt;ArnEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;aws:SourceArn'&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:${AWS::Partition}:scheduler:${AWS::Region}:${AWS::AccountId}:schedule-group/${CampaignScheduleGroup}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The metrics lose granularity, and that is the price. The &lt;code&gt;AWS/Scheduler&lt;/code&gt; namespace has a single dimension, &lt;code&gt;ScheduleGroup&lt;/code&gt;. There is no per-schedule dimension. A group with thirty schedules produces alarms that say something in the group failed without saying what. The dedicated group per campaign is what keeps the alarm specific, and it is a modeling cost the rule does not charge, since &lt;code&gt;AWS/Events&lt;/code&gt; has a &lt;code&gt;RuleName&lt;/code&gt; dimension. This template carries five alarms instead of four, because the trigger side reports &lt;code&gt;TargetErrorCount&lt;/code&gt; and &lt;code&gt;InvocationDroppedCount&lt;/code&gt; as separate signals: the first is the target returning an error on the API call, the second is Scheduler exhausting the retry policy and dropping the invocation. The dead letter queue pair is the same idea as in the rule template, with the names Scheduler uses: &lt;code&gt;InvocationsFailedToBeSentToDeadLetterCount&lt;/code&gt; for the denied write, and queue depth for the write that worked.&lt;/p&gt;

&lt;p&gt;Deploy and inspection, under the scheduler stack name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; iac/eventbridge-scheduler-sns.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;SCHEDULER_STACK_NAME&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cron(0 9 ? * SAT *)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ScheduleTimezone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/Sao_Paulo &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ProjectTag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;weekly-campaigns &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;

aws scheduler get-schedule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; weekly-campaigns-scheduler-saturday-trigger &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-name&lt;/span&gt; weekly-campaigns-scheduler-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected result: the schedule comes back with &lt;code&gt;ScheduleExpressionTimezone&lt;/code&gt; set, the &lt;code&gt;FlexibleTimeWindow&lt;/code&gt; block, and the target carrying &lt;code&gt;RoleArn&lt;/code&gt;, &lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;RetryPolicy&lt;/code&gt; and &lt;code&gt;DeadLetterConfig&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This stack gets torn down the same way, under its own name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation delete-stack &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;SCHEDULER_STACK_NAME&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
aws cloudformation &lt;span class="nb"&gt;wait &lt;/span&gt;stack-delete-complete &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &amp;lt;SCHEDULER_STACK_NAME&amp;gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;REGION&amp;gt; &lt;span class="nt"&gt;--profile&lt;/span&gt; &amp;lt;PROFILE&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That removes the queue, the schedule and the schedule group that contains it, the execution role and the five alarms, plus the topic when the stack created it. One subscription goes with them, if &lt;code&gt;TestSubscriptionEmail&lt;/code&gt; was filled in on that deploy: the stack owns the test subscription even when it does not own the topic, and that is the only subscription of an external topic the teardown touches. The KMS key of this stack carries &lt;code&gt;DeletionPolicy: Retain&lt;/code&gt; as well, so it survives here too and needs its own &lt;code&gt;aws kms schedule-key-deletion&lt;/code&gt;, by key ID once the alias is gone. Recreating the stack under the same name creates a new key, and the retained ones accumulate per cycle, each one billed as a key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs, quotas and production concerns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Delivering to millions of subscribers
&lt;/h3&gt;

&lt;p&gt;The schedule publishes one message a week. All the scale lives on the SNS side, so the useful conversation is about delivery, not about publishing.&lt;/p&gt;

&lt;p&gt;A standard topic accepts 12,500,000 subscriptions, which covers this client's order of magnitude with room to spare. That number does not hold for a FIFO topic, which accepts 100 subscriptions per topic. Anyone considering a FIFO topic for the campaign to gain deduplication meets that limit before anything else.&lt;/p&gt;

&lt;p&gt;Publish throughput per account and region varies: 30,000 messages per second in one group, 9,000 in another, 1,500 in a third, and 300 in the remaining supported regions, &lt;code&gt;sa-east-1&lt;/code&gt; included. For a weekly publish that is irrelevant, and I mention it because it is the number everyone looks up first and it is not the number that matters here.&lt;/p&gt;

&lt;p&gt;What matters is the delivery rate per protocol, and those differ by orders of magnitude.&lt;/p&gt;

&lt;p&gt;SMS has a delivery rate of 20 messages per second, for both promotional and transactional messages. For one million recipients that is 50,000 seconds of delivery, roughly 14 hours. A Saturday 9 AM campaign with a million SMS messages finishes in the early hours of Sunday. That calculation belongs in the conversation before anyone promises a delivery window to the marketing team.&lt;/p&gt;

&lt;p&gt;Before volume there is the spend limit. The monthly SMS spending quota starts at USD 1.00 on a new account, which is a test budget. Production requires a Support case describing the use case, the opt-in process and the message template. The SNS pricing page also records that some countries require dedicated origination identities for sending to work at all, and that since November 1, 2024 SMS charges appear on the bill under AWS End User Messaging. None of those three gets resolved in the week of the campaign.&lt;/p&gt;

&lt;p&gt;Email has a quota of 10 messages per second per &lt;code&gt;email&lt;/code&gt; or &lt;code&gt;email-json&lt;/code&gt; subscription, documented as a hard limit that cannot be raised. Watch the unit: per subscription, not per topic. It does not cap the aggregate fanout across millions of distinct subscriptions. The SNS quota documentation does not publish an aggregate email delivery rate, and I am not going to estimate one.&lt;/p&gt;

&lt;p&gt;Message size: the default is 262,144 bytes, 256 KiB, and it can go up to 1 MiB through the &lt;code&gt;MaximumMessageSize&lt;/code&gt; topic attribute. Each 64 KB chunk published counts as one request for billing.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long SNS keeps retrying a delivery, by endpoint type
&lt;/h3&gt;

&lt;p&gt;The rates above describe deliveries that work. The deliveries that fail follow a schedule SNS sets per protocol, and the alarm threshold in the template comes out of it.&lt;/p&gt;

&lt;p&gt;SNS defines a delivery policy for each delivery protocol, and that policy governs how a server-side failure is retried. The spread between policies is wide. For an AWS managed endpoint, meaning Lambda or SQS, SNS retries up to 100,015 times over 23 days. For a customer managed endpoint, which is where SMTP, SMS and mobile push sit, it is 50 attempts over 6 hours. Same topic, same publish, and the window in which a broken receiver can come back and still get the message differs by orders of magnitude between the two. This campaign lives on the second number: with email and SMS subscriptions, a destination that is down for an afternoon has lost that week's message. HTTP/S is the one protocol where the policy is yours to write, through &lt;code&gt;healthyRetryPolicy&lt;/code&gt; at the topic or the subscription level, and even there the total policy retry time cannot be greater than 3,600 seconds, which the documentation gives as a hard limit that cannot be raised.&lt;/p&gt;

&lt;p&gt;That is also where the &lt;code&gt;DeliveryFailureThreshold&lt;/code&gt; number comes from. &lt;code&gt;numRetries&lt;/code&gt; covers the immediate, pre-backoff, backoff and post-backoff phases together, defaults to 3 and accepts up to 100, so an HTTP/S subscription can turn one undeliverable message into as many as 100 counts on &lt;code&gt;NumberOfNotificationsFailed&lt;/code&gt;. Set the threshold above the retry count of the noisiest subscription on the topic, or the alarm reports a retry storm as a loss.&lt;/p&gt;

&lt;p&gt;Retries are not the default outcome either. The delivery policy governs server-side failure, and for an HTTP/S endpoint the documentation is specific about which responses enter it: 5XX and 429 are retryable, and everything else is treated as a permanent failure. On the other protocols the division the documentation draws is between a server-side error, which the policy covers, and a client-side error, which it does not. A client-side error, which is the category covering a deleted endpoint or an endpoint policy that now refuses SNS, is attempted once and never retried.&lt;/p&gt;

&lt;p&gt;The part that decides whether a failed delivery leaves anything behind is what happens after the policy runs out. SNS stops retrying and discards the message, unless a dead-letter queue is attached to the subscription. That queue is a redrive policy on &lt;code&gt;AWS::SNS::Subscription&lt;/code&gt;, it attaches per subscription rather than per topic, and for a standard topic it points at a standard SQS queue in the same account and region. The dead letter queue in these templates is not that queue. It belongs to the trigger, and it catches a firing that never became a publish. After a successful publish the trigger side has nothing left to catch, so the recoverable evidence requirement is met for the firing and covers nothing past it: the fourth alarm counts the failed deliveries, and the messages behind that count are already gone.&lt;/p&gt;

&lt;p&gt;Subscription dead-letter queues stayed out of both templates, for a reason of ownership rather than cost. The only subscription either stack creates is the test one, and a queue behind a test address records nothing anybody will read. The millions of production subscriptions are not created here and are not in scope for these stacks, so their redrive policies belong to whatever process creates subscriptions, sized per subscription class instead of one queue for the whole topic. Closing the delivery side is real work, and it is work on the subscription, not on the schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  At-least-once delivery and duplicate campaigns
&lt;/h3&gt;

&lt;p&gt;EventBridge delivers with at-least-once semantics. Its own troubleshooting documentation has a section for the case of a rule running more than once in response to a single event, and describes it as rare rather than impossible. EventBridge Scheduler lists at-least-once delivery among the characteristics of the service.&lt;/p&gt;

&lt;p&gt;For a marketing campaign, at-least-once means a risk of the subscriber getting the same email twice. That was exactly the complaint that opened this work, and replacing the human finger with a schedule lowers the probability without eliminating it.&lt;/p&gt;

&lt;p&gt;How it materializes: one occurrence of the schedule produces two target invocations, two &lt;code&gt;sns:Publish&lt;/code&gt; calls, two messages on the topic, two deliveries per subscription. Nothing on the path compares the two.&lt;/p&gt;

&lt;p&gt;The mitigation depends on how strict the requirement is. The identifier in the payload, which is why &lt;code&gt;executionId&lt;/code&gt; exists in the &lt;code&gt;InputTransformer&lt;/code&gt;, makes the duplicate detectable by a consumer: retries of the same occurrence carry the same event &lt;code&gt;id&lt;/code&gt;, and each weekly firing carries a new one. In the Scheduler template there is a difference worth knowing, and it sent me back to the documentation: &lt;code&gt;aws.scheduler.execution-id&lt;/code&gt; is assigned per invocation attempt, not per occurrence. It does not work as a deduplication key. The key that is stable across attempts of the same occurrence is &lt;code&gt;aws.scheduler.scheduled-time&lt;/code&gt;, and an &lt;code&gt;aws.scheduler.attempt-number&lt;/code&gt; above 1 identifies a retry.&lt;/p&gt;

&lt;p&gt;The uncomfortable part: detectable is not the same as deduplicated. With SNS delivering directly to email and SMS, there is no control point between the publish and the subscriber's inbox. To guarantee one delivery per occurrence, the design needs an intermediate step. The topic feeds a queue or a function, that consumer records the occurrence key conditionally, and only then triggers the send. That is a new component to maintain, which is what the minimum-operational-effort requirement was trying to avoid. The choice was made with open eyes: the client accepted a residual risk of rare duplicates in exchange for not having another service on the path, after I showed that the cause of both incidents was human and not infrastructural.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics, alarms and what is opt-in
&lt;/h3&gt;

&lt;p&gt;On the trigger side, in the &lt;code&gt;AWS/Events&lt;/code&gt; namespace with dimension &lt;code&gt;RuleName&lt;/code&gt;: &lt;code&gt;TriggeredRules&lt;/code&gt; and &lt;code&gt;Invocations&lt;/code&gt; for the success path, &lt;code&gt;FailedInvocations&lt;/code&gt; for permanent failure, &lt;code&gt;RetryInvocationAttempts&lt;/code&gt; to see instability that has not become failure yet, &lt;code&gt;ThrottledRules&lt;/code&gt; for runs deferred by throttling, &lt;code&gt;InvocationsSentToDlq&lt;/code&gt; and &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt; for the dead letter path. Several of those are published only when non-zero, which changes how you build an alarm and a dashboard on them.&lt;/p&gt;

&lt;p&gt;On the delivery side, in the &lt;code&gt;AWS/SNS&lt;/code&gt; namespace: &lt;code&gt;NumberOfMessagesPublished&lt;/code&gt;, &lt;code&gt;NumberOfNotificationsDelivered&lt;/code&gt;, &lt;code&gt;NumberOfNotificationsFailed&lt;/code&gt; and &lt;code&gt;NumberOfNotificationsFilteredOut&lt;/code&gt;. The failure count behaves differently per protocol, as described above, so comparing that metric between topics with a different protocol mix leads to the wrong conclusion. SNS metrics are collected at 1-minute intervals, a topic counts as active for up to six hours after the last API call on it, and there is no charge for these metrics.&lt;/p&gt;

&lt;p&gt;What a healthy week looks like on those metrics is worth writing down before building a dashboard, because the numbers are small and an empty graph is the expected state most of the time. Per Saturday, over the 5 minute period that contains the firing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Namespace&lt;/th&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Healthy week&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/Events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;TriggeredRules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RuleName&lt;/code&gt; = &lt;code&gt;weekly-campaigns-rule-saturday-trigger&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;one data point, &lt;code&gt;Sum&lt;/code&gt; of 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/Events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Invocations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;one data point, &lt;code&gt;Sum&lt;/code&gt; of 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/Events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;FailedInvocations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no data point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/Events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;InvocationsSentToDlq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no data point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/SQS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;QueueName&lt;/code&gt; = &lt;code&gt;weekly-campaigns-rule-dlq&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Maximum&lt;/code&gt; of 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/SNS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NumberOfMessagesPublished&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TopicName&lt;/code&gt; = &lt;code&gt;weekly-campaign-subscribers&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;one data point, &lt;code&gt;Sum&lt;/code&gt; of 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/SNS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NumberOfNotificationsDelivered&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Sum&lt;/code&gt; in the order of the confirmed subscriptions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS/SNS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NumberOfNotificationsFailed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;td&gt;no data point, or a &lt;code&gt;Sum&lt;/code&gt; below the alarm threshold&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those are the values this design predicts, not readings I took: no stack was deployed for this post. The useful part is the shape. A single-digit &lt;code&gt;Sum&lt;/code&gt; on the trigger side and a seven-digit &lt;code&gt;Sum&lt;/code&gt; on the delivery side belong to the same firing, and a &lt;code&gt;Sum&lt;/code&gt; of 2 on &lt;code&gt;Invocations&lt;/code&gt; inside one Saturday period is the duplicate the at-least-once section is about.&lt;/p&gt;

&lt;p&gt;A caveat that applies to both sides: the EventBridge documentation states that metric delivery to CloudWatch is best effort, and that a data point may be delayed or not delivered. Metrics serve trend and alarm, not a complete accounting of operations.&lt;/p&gt;

&lt;p&gt;SNS delivery status logging is opt-in, requires an IAM role, and is configured per protocol. It carries a limit that matters for this case: the documentation lists Amazon Data Firehose, Amazon SQS, AWS Lambda, HTTPS and platform application endpoint. Email is not on the list. For a topic whose subscriptions are mostly email, the per-subscription trace that exists is the aggregate metric, not a per-delivery log.&lt;/p&gt;

&lt;p&gt;On Scheduler the metrics live in the &lt;code&gt;AWS/Scheduler&lt;/code&gt; namespace: &lt;code&gt;InvocationAttemptCount&lt;/code&gt;, &lt;code&gt;TargetErrorCount&lt;/code&gt;, &lt;code&gt;TargetErrorThrottledCount&lt;/code&gt;, &lt;code&gt;InvocationThrottleCount&lt;/code&gt;, &lt;code&gt;InvocationDroppedCount&lt;/code&gt;, and, when a dead letter queue is configured, &lt;code&gt;InvocationsSentToDeadLetterCount&lt;/code&gt; and &lt;code&gt;InvocationsFailedToBeSentToDeadLetterCount&lt;/code&gt;. They are kept for 15 months, which covers more than a year of campaigns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What dominates the bill
&lt;/h3&gt;

&lt;p&gt;Prices and quotas consulted on October 8, 2026, with the URLs in the references.&lt;/p&gt;

&lt;p&gt;Scheduling costs close to nothing in both designs, for different reasons. The EventBridge pricing page does not list a charge for scheduled rule invocations on the default event bus, and event delivery to a service in the same account appears as USD 0.00 per million in the calculation examples. Scheduler has a free tier of 14 million invocations per month and charges USD 1.00 per million above that. A weekly campaign produces 52 invocations a year, five orders of magnitude below the free tier.&lt;/p&gt;

&lt;p&gt;The EC2 instance is the only alternative with a relevant fixed cost, and the comparison does not need a price table to be settled: a resource billed by uptime, available 168 hours a week, to run a task of less than a second once a week. Any instance price multiplied by that utilization rate loses to a schedule that is not billed per invocation.&lt;/p&gt;

&lt;p&gt;What dominates the bill is SNS delivery, not scheduling. The pricing structure of a standard topic has two components, API requests and deliveries, and the delivery cost depends on the protocol. Each 64 KB chunk published counts as a request and, except for SMS, each 64 KB chunk delivered counts as a delivery. Data leaving SNS for SQS or Lambda is billed as internet data transfer, regardless of the transfer happening inside the same region. SMS is priced per destination and varies by country, and it tends to be the line item that dominates the bill when it is in the mix. I am not reproducing unit prices, because they are not available in verifiable form on the pricing page without JavaScript rendering, and a wrong price in a post is worse than an absent one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Least privilege, encryption and the account boundary
&lt;/h3&gt;

&lt;p&gt;Who can publish to this topic is the whole security question, because a single &lt;code&gt;sns:Publish&lt;/code&gt; call reaches every confirmed subscription with no approval step in between. There is no staging, no preview and no recall. The blast radius of that permission is the subscriber list.&lt;/p&gt;

&lt;p&gt;The two designs put the permission in different places, and that changes what you audit. In the rule template the publish path lives in the topic resource policy, pinned to the EventBridge service principal, to the ARN of this rule, and to this account, and nothing else in that document grants anything. To answer who can publish, you read one statement. In the Scheduler template the topic has no resource policy at all: the publish is done by the execution role, pinned by its trust policy to this account and to this schedule group, with &lt;code&gt;sns:Publish&lt;/code&gt; scoped to one topic ARN. To answer the same question there, you read one role. Both are narrow, and the operational difference is which artifact a reviewer has to open. What neither of them narrows is the identity policy side: any principal in the account whose IAM policy allows &lt;code&gt;sns:Publish&lt;/code&gt; on this topic can publish, and finding those principals means reading identity policies, not this template.&lt;/p&gt;

&lt;p&gt;Encryption at rest with a customer managed key is in the path because the publisher is a service principal, which is the requirement from the SNS documentation rather than a choice. It protects the message while SNS holds it and it gives the key policy as a second place where the publish can be denied. It stops at the point of delivery. The copy that lands in an inbox and the copy that lands on a phone are readable at the destination, and that is the nature of email and SMS, not a gap in the configuration. For a campaign the content is public by design, so this is not a finding here. For a notification carrying anything about an individual subscriber, the protocol choice is the control, and the encryption of the topic says nothing about it. The absence of delivery status logging for email, covered above, belongs to the same conversation: for most of these subscriptions there is no per-delivery record to audit.&lt;/p&gt;

&lt;p&gt;The account boundary is where this design would need real work. Today the condition on the publish statement is &lt;code&gt;!GetAtt CampaignRule.Arn&lt;/code&gt;, a reference resolved by CloudFormation inside the same stack. A rule in another account has an ARN this template cannot resolve, so the pin becomes a value you maintain by hand, and every account added to the campaign is a change to the topic policy rather than a change in the other account. The encrypted topic adds a second edit in the same move, because the key policy also has to admit the external caller, and the key policy is not written by the stack that owns the rule. On the Scheduler side the problem shifts rather than disappears: the execution role lives with the schedule, so a schedule in another account means a role in that account, and authorizing it is again a statement in this topic policy. The point to carry out of this section is that the single narrow statement that holds today holds because everything sits in one account, and that going cross-account is not a parameter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where this design stops serving
&lt;/h3&gt;

&lt;p&gt;The design solves reliable scheduling for one message that is the same for everybody. It has no notion of a person.&lt;/p&gt;

&lt;p&gt;When the requirement grows to include behavioral segmentation, content templates with per-recipient variables, granular opt-out by campaign type, or open and click reporting, none of that exists in SNS. A topic has subscriptions, and a subscription has a filter policy by attribute or by payload. You can separate audiences with subscription filters up to a point, and past "who opened last week's campaign" you cannot.&lt;/p&gt;

&lt;p&gt;That is where the conversation used to turn to Amazon Pinpoint, and that recommendation no longer holds. AWS set the end of support for Pinpoint on October 30, 2026, two weeks after the date of this post. The service stopped accepting new customers on May 20, 2025, and after the cut-off date the console and the engagement features (&lt;code&gt;endpoints&lt;/code&gt;, &lt;code&gt;segments&lt;/code&gt;, &lt;code&gt;campaigns&lt;/code&gt;, &lt;code&gt;journeys&lt;/code&gt; and analytics) become inaccessible. The SMS, voice, push, OTP and phone number validation APIs are not affected: they were renamed to AWS End User Messaging in the third quarter of 2024 and continue to operate.&lt;/p&gt;

&lt;p&gt;The official migration guide points to two paths. For engagement with segmentation, content templates and campaign orchestration, the proactive engagement solutions of Amazon Connect, including outbound campaigns and Customer Profiles. For email sending, Amazon Simple Email Service, with the Virtual Deliverability Manager replacing the deliverability dashboard. For mobile event collection and analytics, Amazon Kinesis. The guide also says when to stay on End User Messaging with SES instead of moving to Connect: a throughput requirement above the outbound campaign limits, and no need for a human agent or contact orchestration.&lt;/p&gt;

&lt;p&gt;The honest limit of this post: it delivers the trigger, and the guarantee that the trigger leaves a trace. Lifecycle marketing is a different problem, with a different budget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the client ended up with
&lt;/h2&gt;

&lt;p&gt;The Saturday campaign now fires on its own, at 09:00 in Sao Paulo rather than whenever someone got to it on Friday night, described in versioned YAML instead of a shared document with the topic ARN in bold. Over the following three months there was no duplicate firing, no missed firing and no firing at the wrong hour, and the dead letter queue stayed empty. The Friday night procedure stopped existing.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html" rel="noopener noreferrer"&gt;Setting a schedule pattern for scheduled rules (legacy) in Amazon EventBridge&lt;/a&gt;: the six cron fields, the wildcard table, the day-of-month and day-of-week restriction, the UTC+0 annotation on the examples, and the note that scheduled rules are a legacy feature.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-retry-policy.html" rel="noopener noreferrer"&gt;How EventBridge retries delivering events&lt;/a&gt;: the default of 24 hours and up to 185 attempts with exponential backoff and jitter.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html" rel="noopener noreferrer"&gt;Using dead-letter queues to process undelivered events in EventBridge&lt;/a&gt;: standard queues only, same region, message attributes, the errors that skip retries, and the permission the console attaches and the API does not.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html" rel="noopener noreferrer"&gt;Amazon EventBridge input transformation&lt;/a&gt;: predefined variables, including &lt;code&gt;aws.events.event.ingestion-time&lt;/code&gt; and &lt;code&gt;aws.events.rule-name&lt;/code&gt;, and the quoting rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/event-reference.html" rel="noopener noreferrer"&gt;Amazon EventBridge events detail reference&lt;/a&gt;: the &lt;code&gt;Scheduled Event&lt;/code&gt; detail type, &lt;code&gt;source&lt;/code&gt; as &lt;code&gt;aws.events&lt;/code&gt;, &lt;code&gt;detail&lt;/code&gt; with no required fields, and the example event with the rule ARN in &lt;code&gt;resources&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/ref/events-structure.html" rel="noopener noreferrer"&gt;AWS service event metadata&lt;/a&gt;: the fields present in every event delivered to an event bus, &lt;code&gt;id&lt;/code&gt; in UUID format generated per event, and &lt;code&gt;detail&lt;/code&gt; that can be &lt;code&gt;{}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/ref/events-ref-events.html" rel="noopener noreferrer"&gt;Amazon EventBridge events&lt;/a&gt;: &lt;code&gt;Scheduled Event&lt;/code&gt; listed among the events EventBridge sends directly, with &lt;code&gt;source&lt;/code&gt; &lt;code&gt;aws.events&lt;/code&gt; and best effort delivery.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html" rel="noopener noreferrer"&gt;Creating a scheduled rule (legacy) in Amazon EventBridge&lt;/a&gt;: all scheduled events in UTC+0, one minute as the finest resolution, and the delay of several seconds between the trigger and the target running.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-monitoring.html" rel="noopener noreferrer"&gt;Monitoring Amazon EventBridge&lt;/a&gt;: the &lt;code&gt;AWS/Events&lt;/code&gt; metrics, the &lt;code&gt;RuleName&lt;/code&gt; dimension, publication only when non-zero, and best effort metric delivery.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/cross-service-confused-deputy-prevention.html" rel="noopener noreferrer"&gt;Cross-service confused deputy prevention in Amazon EventBridge&lt;/a&gt;: &lt;code&gt;aws:SourceArn&lt;/code&gt; has to be the rule ARN for a target of a rule on an event bus.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-use-resource-based.html" rel="noopener noreferrer"&gt;Using resource-based policies for Amazon EventBridge&lt;/a&gt;: the &lt;code&gt;sns:Publish&lt;/code&gt; statement with principal &lt;code&gt;events.amazonaws.com&lt;/code&gt;, shown without origin conditions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html" rel="noopener noreferrer"&gt;AWS global condition context keys&lt;/a&gt;: &lt;code&gt;aws:SourceAccount&lt;/code&gt; present only when a service principal calls the resource, and &lt;code&gt;aws:PrincipalAccount&lt;/code&gt; present in every request.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-access-policy-language-key-concepts.html" rel="noopener noreferrer"&gt;Amazon SNS access policy language key concepts&lt;/a&gt;: &lt;code&gt;aws:SourceAccount&lt;/code&gt; and &lt;code&gt;aws:SourceOwner&lt;/code&gt; as the pair used when another AWS service publishes to the topic.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-access-policy-use-cases.html" rel="noopener noreferrer"&gt;Example cases for Amazon SNS access control&lt;/a&gt;: the list of services that set &lt;code&gt;SourceAccount&lt;/code&gt; when publishing to a topic, with Amazon EventBridge on it, the deprecation of &lt;code&gt;aws:SourceOwner&lt;/code&gt;, and the note that a new topic is created private with no permissions.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/api/API_CreateTopic.html" rel="noopener noreferrer"&gt;Amazon SNS CreateTopic API reference&lt;/a&gt;: the action is idempotent, and a requester who already owns a topic with the given name gets that topic's ARN back without a new topic being created.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/api/API_GetTopicAttributes.html" rel="noopener noreferrer"&gt;Amazon SNS GetTopicAttributes API reference&lt;/a&gt;: the error list for the operation, where a topic that does not exist is reported with the code &lt;code&gt;NotFound&lt;/code&gt; and HTTP 404.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-troubleshooting.html" rel="noopener noreferrer"&gt;My rule ran more than once in response to one event&lt;/a&gt;: duplicate target invocation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-events-rule-target.html" rel="noopener noreferrer"&gt;AWS::Events::Rule Target&lt;/a&gt;: the &lt;code&gt;InputTransformer&lt;/code&gt;, &lt;code&gt;RetryPolicy&lt;/code&gt;, &lt;code&gt;DeadLetterConfig&lt;/code&gt; and &lt;code&gt;Id&lt;/code&gt; properties.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-sqs-queue.html" rel="noopener noreferrer"&gt;AWS::SQS::Queue&lt;/a&gt;: &lt;code&gt;MessageRetentionPeriod&lt;/code&gt; from 60 to 1,209,600 seconds with a default of 345,600, &lt;code&gt;SqsManagedSseEnabled&lt;/code&gt;, and the two identifiers of a queue, with &lt;code&gt;Ref&lt;/code&gt; returning the URL and &lt;code&gt;GetAtt Arn&lt;/code&gt; the ARN.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-cloudwatch-alarm.html" rel="noopener noreferrer"&gt;AWS::CloudWatch::Alarm&lt;/a&gt;: &lt;code&gt;TreatMissingData&lt;/code&gt; defaulting to &lt;code&gt;missing&lt;/code&gt;, &lt;code&gt;Ref&lt;/code&gt; returning the alarm name, and the alarm ARN in the form &lt;code&gt;alarm:NAME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/service-authorization/latest/reference/list_scheduler.html" rel="noopener noreferrer"&gt;Actions, resources, and condition keys for Amazon EventBridge Scheduler&lt;/a&gt;: the ARN format of a schedule, &lt;code&gt;schedule/${GroupName}/${ScheduleName}&lt;/code&gt;, and of a schedule group, &lt;code&gt;schedule-group/${GroupName}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/kms/latest/developerguide/alias-view.html" rel="noopener noreferrer"&gt;Find the alias name and alias ARN for a KMS key&lt;/a&gt;: the alias ARN format, with &lt;code&gt;alias/NAME&lt;/code&gt; as the resource part.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-scheduler-schedule.html" rel="noopener noreferrer"&gt;AWS::Scheduler::Schedule&lt;/a&gt;: &lt;code&gt;ScheduleExpressionTimezone&lt;/code&gt;, the required &lt;code&gt;FlexibleTimeWindow&lt;/code&gt;, and the expression formats.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-scheduler-schedule-flexibletimewindow.html" rel="noopener noreferrer"&gt;AWS::Scheduler::Schedule FlexibleTimeWindow&lt;/a&gt;: &lt;code&gt;Mode&lt;/code&gt; as &lt;code&gt;OFF&lt;/code&gt; or &lt;code&gt;FLEXIBLE&lt;/code&gt;, &lt;code&gt;MaximumWindowInMinutes&lt;/code&gt; from 1 to 1440.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-properties-scheduler-schedule-target.html" rel="noopener noreferrer"&gt;AWS::Scheduler::Schedule Target&lt;/a&gt;: required &lt;code&gt;RoleArn&lt;/code&gt;, plus &lt;code&gt;Input&lt;/code&gt;, &lt;code&gt;RetryPolicy&lt;/code&gt; and &lt;code&gt;DeadLetterConfig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-targets-templated.html" rel="noopener noreferrer"&gt;Using templated targets in EventBridge Scheduler&lt;/a&gt;: Amazon SNS &lt;code&gt;Publish&lt;/code&gt; as a templated target configured by topic ARN, and the requirement that the execution role carry the permission of the target operation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/configuring-schedule-dlq.html" rel="noopener noreferrer"&gt;Configuring a schedule's dead-letter queue in EventBridge Scheduler&lt;/a&gt;: the DLQ has to be a standard queue and a FIFO queue is not supported, delivery of the dead-letter event is authorized by &lt;code&gt;sqs:SendMessage&lt;/code&gt; on the execution role permission policy, and the message attributes carried on the dead-letter event.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-context-attributes.html" rel="noopener noreferrer"&gt;Adding context attributes in EventBridge Scheduler&lt;/a&gt;: &lt;code&gt;scheduled-time&lt;/code&gt;, &lt;code&gt;execution-id&lt;/code&gt; assigned per attempt, &lt;code&gt;attempt-number&lt;/code&gt;, &lt;code&gt;schedule-arn&lt;/code&gt;, and the documented example formats of each, with &lt;code&gt;scheduled-time&lt;/code&gt; carrying a &lt;code&gt;Z&lt;/code&gt; suffix and &lt;code&gt;execution-id&lt;/code&gt; a short alphanumeric string rather than a UUID.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/cross-service-confused-deputy-prevention.html" rel="noopener noreferrer"&gt;Confused deputy prevention in EventBridge Scheduler&lt;/a&gt;: trust policy with &lt;code&gt;aws:SourceAccount&lt;/code&gt; and the &lt;code&gt;aws:SourceArn&lt;/code&gt; of the schedule group.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/monitoring-cloudwatch.html" rel="noopener noreferrer"&gt;Monitoring Amazon EventBridge Scheduler with Amazon CloudWatch&lt;/a&gt;: the &lt;code&gt;AWS/Scheduler&lt;/code&gt; namespace, the single &lt;code&gt;ScheduleGroup&lt;/code&gt; dimension, the metric list, and 15 month retention.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;What is Amazon EventBridge Scheduler&lt;/a&gt;: at-least-once delivery among the service characteristics.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-key-management.html" rel="noopener noreferrer"&gt;Amazon SNS key management&lt;/a&gt;: the customer managed key requirement for publishing by an AWS service, the &lt;code&gt;kms:GenerateDataKey*&lt;/code&gt; and &lt;code&gt;kms:Decrypt&lt;/code&gt; grant, the principal table listing &lt;code&gt;events.amazonaws.com&lt;/code&gt;, the note that origin conditions are not supported on this path, and &lt;code&gt;KMSAccessDeniedException&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-topic-attributes.html" rel="noopener noreferrer"&gt;Amazon SNS message delivery status&lt;/a&gt;: the protocols supported by delivery status logging.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-monitoring-using-cloudwatch.html" rel="noopener noreferrer"&gt;Monitoring Amazon SNS topics using CloudWatch&lt;/a&gt;: the &lt;code&gt;AWS/SNS&lt;/code&gt; metrics, the per-protocol behavior of &lt;code&gt;NumberOfNotificationsFailed&lt;/code&gt;, the 1-minute interval, the six hour activity window, and the absence of charges.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-message-delivery-retries.html" rel="noopener noreferrer"&gt;Amazon SNS message delivery retries&lt;/a&gt;: one delivery policy per protocol and only HTTP/S customizable, the four phases, 100,015 attempts over 23 days for AWS managed endpoints against 50 attempts over 6 hours for customer managed ones, the 3,600 second hard limit on total HTTP/S retry time, &lt;code&gt;numRetries&lt;/code&gt; defaulting to 3 with a maximum of 100, 5XX and 429 as the retryable errors on the HTTP/S delivery policy, and the message discarded on exhaustion unless the subscription has a dead-letter queue.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html" rel="noopener noreferrer"&gt;Amazon SNS dead-letter queues&lt;/a&gt;: the queue attaches to the subscription rather than the topic, through a redrive policy naming the queue ARN, in the same account and region, with no retry at all for client-side errors.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/general/latest/gr/sns.html" rel="noopener noreferrer"&gt;Amazon SNS endpoints and quotas&lt;/a&gt;: 12,500,000 subscriptions per standard topic and 100 per FIFO topic, the email delivery rate of 10 per second per subscription as a hard limit, 20 per second for promotional and transactional SMS, publish throughput per region, and message size from 256 KiB to 1 MiB.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/sns/latest/dg/channels-sms-awssupport-spend-threshold.html" rel="noopener noreferrer"&gt;Requesting increases to your monthly Amazon SNS SMS spending quota&lt;/a&gt;: the USD 1.00 default on a new account and the increase process.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/sns/pricing/" rel="noopener noreferrer"&gt;Amazon SNS pricing&lt;/a&gt;: the standard topic pricing structure, the 64 KB chunk as the billing unit, transfer to SQS and Lambda billed as internet, dedicated origination identities for SMS in some countries, and SMS charges under AWS End User Messaging since November 1, 2024.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;Amazon EventBridge pricing&lt;/a&gt;: the Scheduler free tier of 14 million invocations per month, USD 1.00 per million above it, and delivery to a service in the same account at USD 0.00 per million.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/pinpoint/latest/userguide/migrate.html" rel="noopener noreferrer"&gt;Amazon Pinpoint end of support&lt;/a&gt;: October 30, 2026, the end of new customers on May 20, 2025, the affected features, the APIs not affected under AWS End User Messaging, and the migration paths to Amazon Connect, SES and Kinesis.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/statemachine-starting.html" rel="noopener noreferrer"&gt;Starting state machine executions in Step Functions&lt;/a&gt;: the list of ways an execution can start, none of them a scheduler internal to the service.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/using-eventbridge-scheduler.html" rel="noopener noreferrer"&gt;Using Amazon EventBridge Scheduler to start a Step Functions state machine execution&lt;/a&gt;: the official path for starting a state machine on a schedule.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/operational-excellence-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework, Operational Excellence pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/reliability-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework, Reliability pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework, Security pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/cost-optimization-pillar/welcome.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework, Cost Optimization pillar&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/wellarchitected/latest/sustainability-pillar/sustainability-pillar.html" rel="noopener noreferrer"&gt;AWS Well-Architected Framework, Sustainability pillar&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>eventbridge</category>
      <category>sns</category>
      <category>cloudformation</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Blocking Container deployments based on vulnerability scans</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:12:33 +0000</pubDate>
      <link>https://dev.to/aws-builders/blocking-container-deployments-based-on-vulnerability-scans-4ko9</link>
      <guid>https://dev.to/aws-builders/blocking-container-deployments-based-on-vulnerability-scans-4ko9</guid>
      <description>&lt;p&gt;I got an AWS question and implemented it to make sure that the option is correct.&lt;/p&gt;

&lt;p&gt;A company runs containerized microservices on ECS Fargate across multiple AWS accounts. The security team has four requirements: scan images before production deployment, block images with CRITICAL vulnerabilities automatically, do it without manual intervention, and centralize all findings in a single audit account.&lt;/p&gt;

&lt;p&gt;Two solutions cover all four requirements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Check these before running &lt;code&gt;terraform apply&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon Inspector must be enabled per account&lt;/strong&gt;&lt;br&gt;
Inspector is not enabled by default. It must be activated in each member account before ECR Enhanced Scanning works. The recommended approach is enabling it through AWS Organizations so new accounts get it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security Hub must be enabled in all accounts&lt;/strong&gt;&lt;br&gt;
Security Hub also requires explicit activation per account and per region. The aggregation feature requires Security Hub to be active in both the member accounts and the central security account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;securityhub.tf&lt;/code&gt; file runs in a different account&lt;/strong&gt;&lt;br&gt;
The Security Hub aggregator resources must be applied to the central security account, not to each member account. The Terraform for that file requires a separate provider alias pointing to the aggregator account. This is covered in the &lt;code&gt;securityhub.tf&lt;/code&gt; section below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform executor permissions&lt;/strong&gt;&lt;br&gt;
The IAM principal running Terraform needs at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;inspector2&lt;/span&gt;:&lt;span class="n"&gt;Enable&lt;/span&gt;
&lt;span class="n"&gt;inspector2&lt;/span&gt;:&lt;span class="n"&gt;AssociateMember&lt;/span&gt;
&lt;span class="n"&gt;securityhub&lt;/span&gt;:&lt;span class="n"&gt;EnableSecurityHub&lt;/span&gt;
&lt;span class="n"&gt;securityhub&lt;/span&gt;:&lt;span class="n"&gt;CreateFindingAggregator&lt;/span&gt;
&lt;span class="n"&gt;securityhub&lt;/span&gt;:&lt;span class="n"&gt;UpdateFindingAggregator&lt;/span&gt;
&lt;span class="n"&gt;ecr&lt;/span&gt;:&lt;span class="n"&gt;PutImageScanningConfiguration&lt;/span&gt;
&lt;span class="n"&gt;ecr&lt;/span&gt;:&lt;span class="n"&gt;DescribeImageScanFindings&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt;:&lt;span class="n"&gt;PutRule&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt;:&lt;span class="n"&gt;PutTargets&lt;/span&gt;
&lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;CreateFunction&lt;/span&gt;
&lt;span class="n"&gt;lambda&lt;/span&gt;:&lt;span class="n"&gt;AddPermission&lt;/span&gt;
&lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;CreateRole&lt;/span&gt;
&lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;AttachRolePolicy&lt;/span&gt;
&lt;span class="n"&gt;iam&lt;/span&gt;:&lt;span class="n"&gt;PutRolePolicy&lt;/span&gt;
&lt;span class="n"&gt;sns&lt;/span&gt;:&lt;span class="n"&gt;CreateTopic&lt;/span&gt;
&lt;span class="n"&gt;sns&lt;/span&gt;:&lt;span class="n"&gt;Subscribe&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt; on each account covers all of these. Lock it down after the initial setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC not required&lt;/strong&gt;&lt;br&gt;
All resources in this solution run outside a VPC. Lambda, EventBridge, Inspector, and Security Hub are all regional services accessed via their public endpoints.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Blocking deployments automatically&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A CI/CD pipeline that deploys on every image push will deploy vulnerable images unless something stops it. The gate needs to be automatic, not dependent on someone reviewing a dashboard. It also needs to be event-driven: when a scan finishes, the result should immediately trigger the enforcement logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The race condition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Inspector Enhanced Scanning does not complete instantly. Depending on image size and layer count, it can take two to five minutes after a push. If the CI/CD pipeline pushes an image and immediately triggers an ECS deployment, the deployment may start before Inspector finishes and before the Lambda gate has a chance to delete the image.&lt;/p&gt;

&lt;p&gt;The safest mitigation is adding a wait step in the pipeline after the push, before the deploy step. A simple approach is polling the Inspector findings API until the scan status returns &lt;code&gt;COMPLETE&lt;/code&gt; or a timeout is reached. If the image has been deleted by the gate Lambda during that window, the deploy step will fail when it tries to pull the image. If no CRITICAL findings exist, the image remains in ECR and the deploy proceeds normally.&lt;/p&gt;

&lt;p&gt;A secondary mitigation is separating the ECR repository from the deploy step by requiring the deploy step to always reference an image by digest rather than by tag. This prevents a race where a new push overwrites a tag that was already scanned clean.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Centralizing findings across accounts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In a multi-account setup, each account has its own Inspector findings. The security team needs a single place to review all findings across all accounts without logging into each one individually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous scanning behavior&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Enhanced Scanning is not a one-time check on push. Inspector re-evaluates images continuously as new CVEs are published. An image that passes the gate today may be flagged tomorrow if a new CRITICAL vulnerability is discovered in one of its packages. The EventBridge rule will fire again for the new finding, and the Lambda gate will delete the image from ECR even if it is already deployed.&lt;/p&gt;

&lt;p&gt;This means deployed containers are not automatically stopped. The gate only prevents new pulls of the vulnerable image. If your ECS task uses &lt;code&gt;imagePullPolicy: always&lt;/code&gt; equivalent behavior or if tasks restart, they will fail to pull the deleted image and the task will not start. This is the intended behavior: the image is gone, and a new clean build is required.&lt;/p&gt;


&lt;h2&gt;
  
  
  The solutions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  ECR Enhanced Scanning with Inspector + EventBridge gate
&lt;/h3&gt;

&lt;p&gt;ECR Enhanced Scanning uses Amazon Inspector instead of the built-in basic scanner. Enhanced Scanning checks OS packages and application dependencies continuously, not just on push. It cross-references findings against multiple vulnerability databases (NVD, vendor advisories) and assigns severity scores more accurately than Basic Scanning.&lt;/p&gt;

&lt;p&gt;When Inspector finishes scanning an image, it emits an event to EventBridge. The event contains the image digest, repository, and a summary of findings by severity. An EventBridge rule filters for events where CRITICAL findings are present and routes them to a Lambda function.&lt;/p&gt;

&lt;p&gt;The Lambda function deletes the image from ECR so it cannot be pulled, then notifies the pipeline via SNS so the deployment step fails with a clear reason.&lt;/p&gt;

&lt;p&gt;The pipeline does not need a separate approval gate or polling loop. It pushes the image, the scan runs, and if CRITICAL vulnerabilities are found the image is deleted before the deployment step can reference it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# inspector.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_inspector2_enabler"&lt;/span&gt; &lt;span class="s2"&gt;"ecr"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_ids&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_caller_identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;resource_types&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ECR"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Inspector enabled for ECR, all repositories in the account get continuous scanning automatically. No per-repository configuration is needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ecr.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecr_repository"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecr_repository_names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="nx"&gt;image_tag_mutability&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"IMMUTABLE"&lt;/span&gt;

  &lt;span class="nx"&gt;image_scanning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;scan_on_push&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;encryption_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;encryption_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AES256"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Clean up untagged images left behind after the gate Lambda deletes a vulnerable image.&lt;/span&gt;
&lt;span class="c1"&gt;# Without this, deleted images accumulate as untagged layers and incur storage cost.&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecr_lifecycle_policy"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;rules&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rulePriority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Remove untagged images immediately"&lt;/span&gt;
        &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;tagStatus&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"untagged"&lt;/span&gt;
          &lt;span class="nx"&gt;countType&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sinceImagePushed"&lt;/span&gt;
          &lt;span class="nx"&gt;countUnit&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"days"&lt;/span&gt;
          &lt;span class="nx"&gt;countNumber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"expire"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;rulePriority&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="nx"&gt;description&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Keep only the last 20 tagged images per repository"&lt;/span&gt;
        &lt;span class="nx"&gt;selection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;tagStatus&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tagged"&lt;/span&gt;
          &lt;span class="nx"&gt;tagPrefixList&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"v"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="nx"&gt;countType&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"imageCountMoreThan"&lt;/span&gt;
          &lt;span class="nx"&gt;countNumber&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"expire"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;code&gt;image_tag_mutability = "IMMUTABLE"&lt;/code&gt; prevents overwriting an existing tag. Combined with the scan gate, this means a tag that passed scanning cannot be replaced with a vulnerable image under the same tag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# iam.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate_lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ECRScanGateLambdaRole"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate_lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ECRScanGateLambdaPolicy"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ECRDeleteImage"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:BatchDeleteImage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:DescribeImages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"ecr:DescribeImageScanFindings"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SNSPublish"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sns:Publish"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Logs"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"logs:CreateLogGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"logs:CreateLogStream"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"logs:PutLogEvents"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:logs:*:*:*"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# lambda.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecr-scan-gate"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.handler"&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python3.12"&lt;/span&gt;
  &lt;span class="nx"&gt;timeout&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

  &lt;span class="nx"&gt;filename&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;SNS_TOPIC_ARN&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"archive_file"&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zip"&lt;/span&gt;
  &lt;span class="nx"&gt;output_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/scan_gate.zip"&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;-&lt;/span&gt;&lt;span class="no"&gt;PYTHON&lt;/span&gt;&lt;span class="sh"&gt;
import boto3
import os
import json

ecr = boto3.client('ecr')
sns = boto3.client('sns')

def handler(event, context):
    detail = event.get('detail', {})
    repository = detail.get('repository-name')
    tag = detail.get('image-tags', ['untagged'])[0]
    digest = detail.get('image-digest')
    findings_summary = detail.get('finding-severity-counts', {})
    critical_count = findings_summary.get('CRITICAL', 0)

    print(f"Repository: {repository}, Tag: {tag}, CRITICAL: {critical_count}")

    if critical_count &amp;gt; 0:
        print(f"Deleting image {repository}:{tag} ({digest}) - {critical_count} CRITICAL findings")

        ecr.batch_delete_image(
            repositoryName=repository,
            imageIds=[{'imageDigest': digest}]
        )

        sns.publish(
            TopicArn=os.environ['SNS_TOPIC_ARN'],
            Subject=f"Deployment blocked: {repository}:{tag}",
            Message=json.dumps({
                'repository': repository,
                'tag': tag,
                'digest': digest,
                'critical_findings': critical_count,
                'action': 'image deleted from ECR',
                'reason': 'CRITICAL vulnerabilities found during Inspector scan'
            }, indent=2)
        )

    return {'statusCode': 200}
&lt;/span&gt;&lt;span class="no"&gt;    PYTHON
&lt;/span&gt;    &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.py"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_permission"&lt;/span&gt; &lt;span class="s2"&gt;"eventbridge"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowEventBridgeInvocation"&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_name&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"events.amazonaws.com"&lt;/span&gt;
  &lt;span class="nx"&gt;source_arn&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecr_scan_critical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# eventbridge.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_rule"&lt;/span&gt; &lt;span class="s2"&gt;"ecr_scan_critical"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecr-inspector-scan-complete"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Triggers when Inspector finds a CRITICAL vulnerability in an ECR image"&lt;/span&gt;

  &lt;span class="nx"&gt;event_pattern&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;source&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws.inspector2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;detail-type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Inspector2 Finding"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AWS_ECR_CONTAINER_IMAGE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;severity&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CRITICAL"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudwatch_event_target"&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate_lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;rule&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_event_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecr_scan_critical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ScanGateLambda"&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# sns.tf&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic"&lt;/span&gt; &lt;span class="s2"&gt;"scan_alerts"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ecr-scan-critical-alerts"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_sns_topic_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;topic_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;protocol&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"email"&lt;/span&gt;
  &lt;span class="nx"&gt;endpoint&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pipeline_notification_email&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Inspector findings to Security Hub with cross-account aggregation
&lt;/h3&gt;

&lt;p&gt;Inspector publishes all ECR findings directly to Security Hub with no additional configuration beyond enabling both services. Security Hub normalizes findings into the ASFF (Amazon Security Finding Format), making them queryable and filterable in a consistent way regardless of which account or scanner produced them.&lt;/p&gt;

&lt;p&gt;Security Hub's finding aggregation feature designates one account as the aggregator. All member accounts linked through Organizations forward their findings to the aggregator account automatically. The security team reviews findings in one place, sets up alerting once, and writes queries once.&lt;/p&gt;

&lt;p&gt;No custom code. No cross-account Lambda invocations. No S3 buckets collecting logs from each account.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;securityhub.tf&lt;/code&gt; below must be applied to the &lt;strong&gt;central security account&lt;/strong&gt;, not to member accounts. Use a separate Terraform provider alias pointing to that account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# securityhub.tf&lt;/span&gt;
&lt;span class="c1"&gt;# Apply this with the security account provider alias, not the default provider.&lt;/span&gt;
&lt;span class="c1"&gt;# Example provider configuration in main.tf:&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# provider "aws" {&lt;/span&gt;
&lt;span class="c1"&gt;#   alias  = "security"&lt;/span&gt;
&lt;span class="c1"&gt;#   region = var.aws_region&lt;/span&gt;
&lt;span class="c1"&gt;#   assume_role {&lt;/span&gt;
&lt;span class="c1"&gt;#     role_arn = "arn:aws:iam::${var.security_hub_aggregator_account_id}:role/TerraformRole"&lt;/span&gt;
&lt;span class="c1"&gt;#   }&lt;/span&gt;
&lt;span class="c1"&gt;# }&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_securityhub_account"&lt;/span&gt; &lt;span class="s2"&gt;"aggregator"&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="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_securityhub_finding_aggregator"&lt;/span&gt; &lt;span class="s2"&gt;"main"&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="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;
  &lt;span class="nx"&gt;linking_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ALL_REGIONS"&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_securityhub_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_securityhub_member"&lt;/span&gt; &lt;span class="s2"&gt;"members"&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="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;member_account_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;account_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;
  &lt;span class="nx"&gt;invite&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_securityhub_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregator&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_securityhub_product_subscription"&lt;/span&gt; &lt;span class="s2"&gt;"inspector"&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="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;security&lt;/span&gt;
  &lt;span class="nx"&gt;product_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:securityhub:${var.aws_region}::product/aws/inspector"&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_securityhub_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aggregator&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;h2&gt;
  
  
  Full variable list
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# variables.tf&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"ecr_repository_names"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"List of ECR repository names to protect"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"pipeline_notification_email"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Email address that receives notifications when a deployment is blocked"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"security_hub_aggregator_account_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS account ID of the central security account that aggregates findings"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"member_account_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"List of member account IDs to link to the Security Hub aggregator"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&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;h2&gt;
  
  
  Outputs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# outputs.tf&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"scan_gate_lambda_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ARN of the Lambda function that enforces the scan gate"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"scan_alerts_topic_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SNS topic ARN that receives blocked deployment notifications"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_sns_topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scan_alerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"ecr_repository_urls"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ECR repository URLs for the CI/CD pipeline"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;aws_ecr_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_url&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;h2&gt;
  
  
  Validating the deployment
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Check 1: Inspector is enabled for ECR&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;aws inspector2 list-coverage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter-criteria&lt;/span&gt; &lt;span class="s1"&gt;'{"resourceType":[{"comparison":"EQUALS","value":"AWS_ECR_CONTAINER_IMAGE"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'coveredResources[0].scanStatus.statusCode'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: "ACTIVE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 2: EventBridge rule is active&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;aws events describe-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; ecr-inspector-scan-complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'State'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: "ENABLED"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 3: trigger the gate with a known vulnerable image&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Push an image with a known CRITICAL vulnerability to one of the protected repositories. Inspector will scan it within a few minutes and the EventBridge rule will fire. Confirm the image was deleted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr list-images &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; &amp;lt;your-repo-name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'imageIds[*]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image should no longer appear. Check the SNS email for the blocked deployment notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 4: Security Hub is receiving Inspector findings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this from the aggregator account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws securityhub get-findings &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s1"&gt;'{"ProductName":[{"Value":"Inspector","Comparison":"EQUALS"}]}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Findings[0].{Title:Title,Severity:Severity.Label,Account:AwsAccountId}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;aggregator-region&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings from all member accounts should appear here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the other options do not work?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1: Basic Scanning with CodePipeline approval gate&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Basic Scanning only runs on push and checks OS packages against a single vulnerability feed. It misses application-layer vulnerabilities that Inspector catches. The CodePipeline approval gate also requires per-pipeline integration: every pipeline that deploys from ECR needs its own gate, and each gate needs to know which image it is checking. This scales poorly across multiple repositories and pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2: CloudWatch metric filter on ECR scan events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ECR scan result events go to EventBridge natively. Routing them through CloudWatch Logs first adds a step with no benefit. You lose the structured event payload, need a metric filter pattern to extract severity, and then need to reconnect back to a Lambda anyway. EventBridge is the direct path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3: AWS Config with ecr-image-scan-on-push rule&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Config managed rule checks whether scan-on-push is enabled for a repository. It evaluates repository configuration, not scan results. It has no visibility into whether a scan found CRITICAL vulnerabilities. Remediating non-compliance means enabling scan-on-push on repositories that have it disabled, not blocking images based on findings.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The two flows run in parallel. Inspector feeds both the blocking gate and the centralized audit trail at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI/CD pipeline
    |
    v
docker push --&amp;gt; ECR repository (IMMUTABLE tags)
                    |
              Inspector scans image (2-5 min)
                    |
          +---------+---------+
          |                   |
          v                   v
   EventBridge rule     Security Hub
   (CRITICAL finding)   (all findings, all accounts)
          |                   |
          v                   v
   Lambda gate          Aggregator account
          |             Single audit view
   CRITICAL &amp;gt; 0?
          |
     Yes  |  No
     |         |
     v         v
  Delete    Image stays
  image     Pipeline deploys
  from ECR  normally
     |
  SNS notification
  Pipeline fails
  (image not found)

Inspector also re-evaluates images continuously.
New CVE published --&amp;gt; new EventBridge event --&amp;gt; gate fires again.
Deployed containers are not stopped automatically,
but the image is removed from ECR so no new tasks can pull it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cost to keep in mind
&lt;/h2&gt;

&lt;p&gt;Amazon Inspector charges per container image scanned per month. The first 2,500 images per account per month are included in the free tier during the first 30 days. After that it charges per image. Security Hub charges per finding ingested, with a free tier of 10,000 findings per month per account for the first 12 months.&lt;/p&gt;

&lt;p&gt;For a multi-account environment with active CI/CD pipelines, Inspector and Security Hub costs scale with the number of images pushed. The ECR lifecycle policy prevents untagged layer accumulation from inflating storage costs when the gate deletes images.&lt;/p&gt;

</description>
      <category>ecs</category>
      <category>security</category>
      <category>aws</category>
      <category>awsinspector</category>
    </item>
    <item>
      <title>RDS High Availability and credential rotation without downtime</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Tue, 18 Aug 2026 18:54:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/rds-high-availability-and-credential-rotation-without-downtime-5fpj</link>
      <guid>https://dev.to/aws-builders/rds-high-availability-and-credential-rotation-without-downtime-5fpj</guid>
      <description>&lt;p&gt;I got an AWS question and implemented it to make sure that the option is correct.&lt;/p&gt;

&lt;p&gt;A critical financial application runs on RDS for PostgreSQL. The requirements are tight: 1-second RPO, 60-second RTO, and database credentials rotated every 30 days without taking the application offline.&lt;/p&gt;

&lt;p&gt;Two independent problems. Two independent solutions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Check these before running &lt;code&gt;terraform apply&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RDS Proxy availability&lt;/strong&gt;&lt;br&gt;
RDS Proxy is not available on all instance types. It requires instances with at least 2 vCPUs. &lt;code&gt;db.t3.micro&lt;/code&gt; is not supported. &lt;code&gt;db.t3.medium&lt;/code&gt; and above work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Terraform executor permissions&lt;/strong&gt;&lt;br&gt;
The IAM principal running Terraform needs, at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rds:CreateDBInstance
rds:CreateDBProxy
rds:CreateDBProxyTargetGroup
rds:RegisterDBProxyTargets
rds:ModifyDBInstance
iam:CreateRole
iam:AttachRolePolicy
iam:PutRolePolicy
iam:PassRole
secretsmanager:CreateSecret
secretsmanager:PutSecretValue
secretsmanager:RotateSecret
lambda:CreateFunction
lambda:AddPermission
ec2:CreateSecurityGroup
ec2:AuthorizeSecurityGroupIngress
ec2:CreateDBSubnetGroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt; on the account covers all of these. Lock it down after the initial setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC requirements&lt;/strong&gt;&lt;br&gt;
RDS Proxy runs inside your VPC. You need at least two private subnets in different Availability Zones. The rotation Lambda also runs inside the VPC so it can reach the RDS instance directly during the credential update step.&lt;/p&gt;


&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Database failure recovery&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RPO of 1 second means almost no data loss is acceptable. RTO of 60 seconds means the application must resume within a minute of a failure. A standard single-instance RDS setup fails both requirements: there is no automatic failover, and restoring from a backup takes far longer than 60 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credential rotation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rotating credentials on a schedule sounds simple until you factor in application downtime. If you update a password and the application still holds connections authenticated with the old one, those connections fail. The rotation mechanism needs to handle the transition without the application noticing.&lt;/p&gt;


&lt;h2&gt;
  
  
  The solutions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Multi-AZ + RDS Proxy
&lt;/h3&gt;

&lt;p&gt;RDS Multi-AZ keeps a synchronous standby replica in a second Availability Zone. Every write to the primary is committed to the standby before the transaction is acknowledged. This gives you RPO close to zero, typically under 1 second for PostgreSQL.&lt;/p&gt;

&lt;p&gt;When the primary fails, RDS automatically promotes the standby. The DNS record for the cluster endpoint is updated to point to the new primary. Failover typically completes in 60 seconds or less, which meets the RTO requirement.&lt;/p&gt;

&lt;p&gt;The problem is what happens to application connections during that DNS update. Existing database connections are dropped. If your application has a connection pool, it will spend time re-establishing connections and waiting for DNS propagation. Under load, this causes errors.&lt;/p&gt;

&lt;p&gt;RDS Proxy sits between the application and the database. The application connects to the Proxy endpoint, not the RDS endpoint directly. The Proxy maintains a warm pool of connections to the database. During a Multi-AZ failover, the Proxy detects the new primary and reroutes connections internally. The application connection to the Proxy stays alive. From the application's perspective, the failover is nearly invisible.&lt;/p&gt;

&lt;p&gt;Without the Proxy, Multi-AZ alone may technically meet 60-second RTO on paper but cause application-level errors during the DNS transition. With the Proxy, the application keeps working through the failover.&lt;/p&gt;
&lt;h3&gt;
  
  
  Secrets Manager with automatic rotation
&lt;/h3&gt;

&lt;p&gt;Secrets Manager stores the database credentials and handles rotation through a managed Lambda function. AWS provides a built-in rotation Lambda for RDS PostgreSQL that handles the full rotation cycle: generate a new password, update it in RDS, update the secret, verify the new credentials work.&lt;/p&gt;

&lt;p&gt;The rotation Lambda follows a four-step process using staging labels (&lt;code&gt;AWSPENDING&lt;/code&gt;, &lt;code&gt;AWSCURRENT&lt;/code&gt;, &lt;code&gt;AWSPREVIOUS&lt;/code&gt;). The new password is set in the database before it becomes &lt;code&gt;AWSCURRENT&lt;/code&gt;. This means both the old and new passwords are valid during the transition window. Applications retrieving credentials from Secrets Manager at runtime always get the current valid credentials without any downtime.&lt;/p&gt;

&lt;p&gt;RDS Proxy reads credentials from Secrets Manager directly. The application connects to the Proxy endpoint and never touches credentials directly. When Secrets Manager rotates the password, the Proxy picks up the new credentials on its next refresh cycle. The application connection to the Proxy stays alive throughout.&lt;/p&gt;


&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;
&lt;h3&gt;
  
  
  File structure
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── variables.tf
├── networking.tf
├── iam.tf
├── rds.tf
├── secrets.tf
└── outputs.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;variables.tf&lt;/code&gt;
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"aws_region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS region to deploy resources"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"vpc_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ID of the VPC where RDS and RDS Proxy will run"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"private_subnet_ids"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"List of private subnet IDs in at least two AZs"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"app_security_group_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Security group ID of the application servers that connect to RDS Proxy"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"db_password"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Initial database password (Secrets Manager takes over after first rotation)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;networking.tf&lt;/code&gt;: security groups
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Security group for the RDS instance&lt;/span&gt;
&lt;span class="c1"&gt;# Accepts connections from RDS Proxy and the rotation Lambda only&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"rds"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-rds"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow PostgreSQL access from RDS Proxy and rotation Lambda"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL from RDS Proxy"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Security group for RDS Proxy&lt;/span&gt;
&lt;span class="c1"&gt;# Accepts connections from the application tier&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"rds_proxy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-rds-proxy"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow PostgreSQL access from application servers"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;

  &lt;span class="nx"&gt;ingress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PostgreSQL from application"&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
    &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app_security_group_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Security group for the rotation Lambda&lt;/span&gt;
&lt;span class="c1"&gt;# Needs outbound access to RDS and to the Secrets Manager VPC endpoint (or internet)&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"rotation_lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-rotation-lambda"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow rotation Lambda to reach RDS and Secrets Manager"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_id&lt;/span&gt;

  &lt;span class="nx"&gt;egress&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;from_port&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;to_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_blocks&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# DB subnet group: RDS requires subnets in at least two AZs&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_subnet_group"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-db-subnet-group"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-db-subnet-group"&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;h3&gt;
  
  
  &lt;code&gt;iam.tf&lt;/code&gt;: roles for RDS Proxy and rotation Lambda
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Role for RDS Proxy to read credentials from Secrets Manager&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"rds_proxy_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDSProxySecretsManagerRole"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rds.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"rds_proxy_secrets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDSProxySecretsManagerPolicy"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"secretsmanager:GetSecretValue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"secretsmanager:DescribeSecret"&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&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;h3&gt;
  
  
  &lt;code&gt;rds.tf&lt;/code&gt;: RDS instance and Proxy
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_instance"&lt;/span&gt; &lt;span class="s2"&gt;"primary"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;identifier&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-db"&lt;/span&gt;
  &lt;span class="nx"&gt;engine&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
  &lt;span class="nx"&gt;engine_version&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"15.4"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_class&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"db.t3.medium"&lt;/span&gt;
  &lt;span class="nx"&gt;allocated_storage&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
  &lt;span class="nx"&gt;db_name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financialdb"&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dbadmin"&lt;/span&gt;
  &lt;span class="nx"&gt;password&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;
  &lt;span class="nx"&gt;multi_az&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;storage_encrypted&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;backup_retention_period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="nx"&gt;skip_final_snapshot&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;db_subnet_group_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_subnet_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-db"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-proxy"&lt;/span&gt;
  &lt;span class="nx"&gt;debug_logging&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;engine_family&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"POSTGRESQL"&lt;/span&gt;
  &lt;span class="nx"&gt;idle_client_timeout&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;
  &lt;span class="nx"&gt;require_tls&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rds_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_subnet_ids&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;

  &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auth_scheme&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SECRETS"&lt;/span&gt;
    &lt;span class="nx"&gt;iam_auth&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DISABLED"&lt;/span&gt;
    &lt;span class="nx"&gt;secret_arn&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-proxy"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy_default_target_group"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db_proxy_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;connection_pool_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;connection_borrow_timeout&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;
    &lt;span class="nx"&gt;max_connections_percent&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
    &lt;span class="nx"&gt;max_idle_connections_percent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_db_proxy_target"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;db_instance_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;
  &lt;span class="nx"&gt;db_proxy_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;target_group_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy_default_target_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The application connects to &lt;code&gt;aws_db_proxy.main.endpoint&lt;/code&gt;, not to the RDS instance endpoint directly.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;code&gt;secrets.tf&lt;/code&gt;: Secrets Manager and rotation
&lt;/h3&gt;

&lt;p&gt;The rotation Lambda comes from the AWS Serverless Application Repository. AWS maintains it. You do not write or host any rotation code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"db_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app/db-credentials"&lt;/span&gt;
  &lt;span class="nx"&gt;recovery_window_in_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;

  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financial-app-db-credentials"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_version"&lt;/span&gt; &lt;span class="s2"&gt;"db_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;secret_string&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;username&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dbadmin"&lt;/span&gt;
    &lt;span class="nx"&gt;password&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_password&lt;/span&gt;
    &lt;span class="nx"&gt;engine&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"postgres"&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;
    &lt;span class="nx"&gt;dbname&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"financialdb"&lt;/span&gt;
    &lt;span class="nx"&gt;dbInstanceIdentifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Deploy the AWS-managed rotation Lambda from the Serverless Application Repository&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_serverlessapplicationrepository_cloudformation_stack"&lt;/span&gt; &lt;span class="s2"&gt;"rotation_lambda"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SecretsManagerRDSPostgreSQLRotationSingleUser"&lt;/span&gt;
  &lt;span class="nx"&gt;application_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSPostgreSQLRotationSingleUser"&lt;/span&gt;
  &lt;span class="nx"&gt;semantic_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.1.387"&lt;/span&gt;

  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"CAPABILITY_IAM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"CAPABILITY_RESOURCE_POLICY"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;endpoint&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://secretsmanager.${var.aws_region}.amazonaws.com"&lt;/span&gt;
    &lt;span class="nx"&gt;functionName&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SecretsManagerRDSPostgreSQLRotation"&lt;/span&gt;
    &lt;span class="nx"&gt;vpcSubnetIds&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;vpcSecurityGroupIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"rotation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SecretsManagerRDSPostgreSQLRotation"&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_serverlessapplicationrepository_cloudformation_stack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation_lambda&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_permission"&lt;/span&gt; &lt;span class="s2"&gt;"secrets_manager"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowSecretsManagerInvocation"&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_name&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secretsmanager.amazonaws.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_rotation"&lt;/span&gt; &lt;span class="s2"&gt;"db_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;rotation_lambda_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;rotation_rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;automatically_after_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_lambda_permission&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secrets_manager&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;h3&gt;
  
  
  &lt;code&gt;outputs.tf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"proxy_endpoint"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Connect the application to this endpoint, not to the RDS instance directly"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_proxy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;endpoint&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"secret_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ARN of the Secrets Manager secret storing database credentials"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db_credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"rds_instance_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RDS instance identifier"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identifier&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Validating the deployment
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Check 1: Multi-AZ is active&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;aws rds describe-db-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-instance-identifier&lt;/span&gt; financial-app-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'DBInstances[0].MultiAZ'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 2: RDS Proxy is available and connected to the instance&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;aws rds describe-db-proxies &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-proxy-name&lt;/span&gt; financial-app-proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'DBProxies[0].Status'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: "available"&lt;/span&gt;

aws rds describe-db-proxy-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-proxy-name&lt;/span&gt; financial-app-proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Targets[*].{Endpoint:Endpoint,State:TargetHealth.State}'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: State "AVAILABLE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 3: rotation is configured and working&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;aws secretsmanager describe-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-id&lt;/span&gt; financial-app/db-credentials &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'{RotationEnabled:RotationEnabled,LastRotatedDate:LastRotatedDate,NextRotationDate:NextRotationDate}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To trigger a manual rotation immediately and confirm the Lambda executes without errors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager rotate-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-id&lt;/span&gt; financial-app/db-credentials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check the rotation status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws secretsmanager describe-secret &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--secret-id&lt;/span&gt; financial-app/db-credentials &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'RotationRules'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 4: simulate a failover&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RDS provides a reboot-with-failover option that forces the standby to become primary. This lets you observe the actual failover behavior before a real incident:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds reboot-db-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--db-instance-identifier&lt;/span&gt; financial-app-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--force-failover&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Monitor the event log during the failover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws rds describe-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-identifier&lt;/span&gt; financial-app-db &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-type&lt;/span&gt; db-instance &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--duration&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Events[*].{Time:Date,Message:Message}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application should continue serving requests through the Proxy during this window. Check your application logs for connection errors. If the Proxy is working correctly, you should see none.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the other options do not work?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option: automated backups with PITR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;PITR restores to a point in time by replaying transaction logs onto a snapshot. This creates a new RDS instance. Provisioning a new instance, restoring data, and updating connection strings takes 15 to 30 minutes at minimum. The 60-second RTO is not achievable with this approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option: Parameter Store with EventBridge rotation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Parameter Store does not have native RDS credential rotation. You write the rotation logic yourself in Lambda, manage the staging window yourself, and handle the transition between old and new credentials yourself. This is exactly what Secrets Manager already does, with managed Lambda functions and staging label support built in. Option D reimplements a subset of Secrets Manager without the reliability guarantees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option: read replica with manual promotion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A read replica provides an asynchronous copy of the data. Promotion requires manual steps: stopping replication, promoting the replica, updating the application connection string. The process takes several minutes and requires human intervention. Neither the RTO nor the automation requirement is met.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the architecture looks like end to end
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
    |
    v
RDS Proxy endpoint
    |
    |-- Normal operation --&amp;gt; RDS Primary (AZ-a)
    |                             |
    |                         Synchronous replication
    |                             |
    |                        RDS Standby (AZ-b)
    |
    |-- During failover --&amp;gt; Proxy detects new primary
                            Proxy reroutes internally
                            Application connection stays alive

Secrets Manager (30-day rotation schedule)
    |
    v
Rotation Lambda (inside VPC)
    |-- Updates password in RDS
    |-- Updates AWSCURRENT in Secrets Manager
    |
RDS Proxy reads new credentials on next refresh
Application never sees the credential change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cost to keep in mind
&lt;/h2&gt;

&lt;p&gt;Multi-AZ doubles the instance cost because the standby runs continuously. RDS Proxy charges per vCPU of the underlying RDS instance per hour. Secrets Manager charges $0.40 per secret per month plus $0.05 per 10,000 API calls.&lt;/p&gt;

&lt;p&gt;For a financial application with a 1-second RPO and 60-second RTO requirement, the alternative to this setup is manual failover, potential data loss, and credential management code you have to maintain and test yourself.&lt;/p&gt;

</description>
      <category>database</category>
      <category>aws</category>
      <category>terraform</category>
      <category>cloudoperations</category>
    </item>
    <item>
      <title>Enforcing SSE-KMS on S3 buckets across all AWS accounts</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Thu, 16 Jul 2026 15:05:08 +0000</pubDate>
      <link>https://dev.to/aws-builders/enforcing-sse-kms-on-s3-buckets-across-all-aws-accounts-215</link>
      <guid>https://dev.to/aws-builders/enforcing-sse-kms-on-s3-buckets-across-all-aws-accounts-215</guid>
      <description>&lt;p&gt;I got an AWS question and implemented it to make sure that the option is correct.&lt;/p&gt;

&lt;p&gt;You have 30 AWS accounts under Organizations. A developer in one of them creates an S3 bucket, skips encryption, and now you have a compliance gap. By the time your next audit runs, there are three more buckets in the same state.&lt;/p&gt;

&lt;p&gt;This post covers how to close that gap permanently: stopping unencrypted buckets before they are created, and fixing the ones that already exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Check these before running &lt;code&gt;terraform apply&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AWS Organizations&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organizations are enabled, and SCPs are active. Go to the management account → AWS Organizations → Policies → Service Control Policies and confirm it shows "Enabled". If SCPs are not enabled, the policy in this post will be created but will have no effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Terraform executor permissions&lt;/strong&gt;&lt;br&gt;
The IAM principal running Terraform needs, at minimum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;organizations:CreatePolicy
organizations:AttachPolicy
organizations:DescribePolicy
iam:CreateRole
iam:AttachRolePolicy
iam:PassRole
config:PutConfigRule
config:PutRemediationConfigurations
config:PutConfigurationRecorder
config:PutDeliveryChannel
config:StartConfigurationRecorder
cloudformation:CreateStackSet
cloudformation:CreateStackInstances
cloudformation:DescribeStackSet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AdministratorAccess&lt;/code&gt; on the management account covers all of these. Lock it down after the initial setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Config delivery bucket&lt;/strong&gt;&lt;br&gt;
AWS Config needs an S3 bucket to store configuration snapshots and history. The bucket must exist before you run &lt;code&gt;terraform apply&lt;/code&gt;, and it needs a specific bucket policy that allows Config to write to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config_bucket.tf - create this before the rest&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"config_delivery"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"config-delivery-${data.aws_caller_identity.current.account_id}"&lt;/span&gt;
  &lt;span class="nx"&gt;force_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_policy"&lt;/span&gt; &lt;span class="s2"&gt;"config_delivery"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSConfigBucketPermissionsCheck"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"config.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:GetBucketAcl"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSConfigBucketDelivery"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"config.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:PutObject"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_s3_bucket.config_delivery.arn}/AWSLogs/*/Config/*"&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringEquals&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"s3:x-amz-acl"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"bucket-owner-full-control"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_caller_identity"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this exact bucket policy, Config will fail to start the recorder, and you will see &lt;code&gt;InsufficientDeliveryPolicyException&lt;/code&gt; errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  How does the solution work?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SCP: the preventive layer
&lt;/h3&gt;

&lt;p&gt;A Service Control Policy attached to the root OU acts as a guardrail across every account in the organization. It does not grant permissions. It restricts what IAM can allow. Even an account admin cannot override it.&lt;/p&gt;

&lt;p&gt;The policy denies &lt;code&gt;s3:CreateBucket&lt;/code&gt; unless the request includes the SSE-KMS header. The bucket never gets created if encryption is not specified.&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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DenyS3CreateBucketWithoutSSEKMS"&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;"Deny"&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;"s3:CreateBucket"&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;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&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;"StringNotEqualsIfExists"&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;"s3:x-amz-server-side-encryption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws:kms"&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;"Null"&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;"s3:x-amz-server-side-encryption"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"true"&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;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;The &lt;code&gt;StringNotEqualsIfExists&lt;/code&gt; + &lt;code&gt;Null&lt;/code&gt; combination covers two cases: requests that include the header with the wrong value, and requests that omit the header entirely. Using only &lt;code&gt;StringNotEquals&lt;/code&gt; would miss the second case.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Config: the detective and corrective layer
&lt;/h3&gt;

&lt;p&gt;For buckets that already exist, Config evaluates each one against the &lt;code&gt;s3-bucket-server-side-encryption-enabled&lt;/code&gt; managed rule. When it finds a non-compliant bucket, it triggers an SSM Automation document that applies SSE-KMS automatically.&lt;/p&gt;

&lt;p&gt;No manual intervention. No Lambda to maintain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform
&lt;/h2&gt;

&lt;p&gt;The files below create the SCP, the delivery bucket for Config, the Config rule with remediation for the management account, and deploy the same Config rule to all member accounts using a StackSet.&lt;/p&gt;

&lt;h3&gt;
  
  
  File structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── main.tf
├── variables.tf
├── config_bucket.tf
├── scp.tf
├── config.tf
├── iam.tf
└── stackset.tf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;variables.tf&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"organization_root_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Root ID of the AWS Organization (e.g. r-xxxx)"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"kms_key_arn"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ARN of the KMS key to use for SSE-KMS enforcement"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;scp.tf&lt;/code&gt;: create and attach the SCP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_organizations_policy"&lt;/span&gt; &lt;span class="s2"&gt;"s3_require_ssekms"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RequireS3SSEKMS"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Denies creation of S3 buckets without SSE-KMS"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SERVICE_CONTROL_POLICY"&lt;/span&gt;

  &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DenyS3CreateBucketWithoutSSEKMS"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deny"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3:CreateBucket"&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
        &lt;span class="nx"&gt;Condition&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;StringNotEqualsIfExists&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"s3:x-amz-server-side-encryption"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;Null&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s2"&gt;"s3:x-amz-server-side-encryption"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_organizations_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"s3_ssekms_root"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;policy_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_organizations_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_require_ssekms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization_root_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Attaching to the root OU means every account in the organization is covered, including accounts added in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;iam.tf&lt;/code&gt;: roles for Config and SSM remediation
&lt;/h3&gt;

&lt;p&gt;Two roles are needed. The first allows Config to record and deliver configuration data. The second allows SSM Automation to call &lt;code&gt;s3:PutEncryptionConfiguration&lt;/code&gt; during remediation. Without this second role, auto-remediation will trigger but fail silently. Config marks the execution as failed and retries up to the &lt;code&gt;maximum_automatic_attempts&lt;/code&gt; limit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Role for AWS Config recorder&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"config_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWSConfigRole"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"config.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy_attachment"&lt;/span&gt; &lt;span class="s2"&gt;"config_managed"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;policy_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:iam::aws:policy/service-role/AWS_ConfigRole"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Role for SSM Automation remediation&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role"&lt;/span&gt; &lt;span class="s2"&gt;"ssm_remediation_role"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSMRemediationS3SSEKMSRole"&lt;/span&gt;

  &lt;span class="nx"&gt;assume_role_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
      &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
      &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_iam_role_policy"&lt;/span&gt; &lt;span class="s2"&gt;"ssm_remediation_policy"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSMRemediationS3SSEKMSPolicy"&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm_remediation_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
    &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowS3Encryption"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"s3:PutEncryptionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="s2"&gt;"s3:GetEncryptionConfiguration"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Sid&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowKMSDescribe"&lt;/span&gt;
        &lt;span class="nx"&gt;Effect&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
        &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
          &lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kms_key_arn&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;h3&gt;
  
  
  &lt;code&gt;config.tf&lt;/code&gt; - Config rule and remediation for the management account
&lt;/h3&gt;

&lt;p&gt;If you also want to enforce this on the management account itself, add the rule directly. Member accounts are handled via the StackSet below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_config_config_rule"&lt;/span&gt; &lt;span class="s2"&gt;"s3_sse_enabled"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-bucket-server-side-encryption-enabled"&lt;/span&gt;

  &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;owner&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
    &lt;span class="nx"&gt;source_identifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_config_configuration_recorder_status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_config_remediation_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"s3_sse_remediation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;config_rule_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_config_config_rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3_sse_enabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;target_type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSM_DOCUMENT"&lt;/span&gt;
  &lt;span class="nx"&gt;target_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS-EnableS3BucketEncryption"&lt;/span&gt;

  &lt;span class="nx"&gt;automatic&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;maximum_automatic_attempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
  &lt;span class="nx"&gt;retry_attempt_seconds&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;

  &lt;span class="c1"&gt;# The role ARN is required - without it SSM has no permissions to act&lt;/span&gt;
  &lt;span class="nx"&gt;resource_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::S3::Bucket"&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AutomationAssumeRole"&lt;/span&gt;
    &lt;span class="nx"&gt;static_value&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssm_remediation_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"BucketName"&lt;/span&gt;
    &lt;span class="nx"&gt;resource_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RESOURCE_ID"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSEAlgorithm"&lt;/span&gt;
    &lt;span class="nx"&gt;static_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;parameter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KMSMasterKeyID"&lt;/span&gt;
    &lt;span class="nx"&gt;static_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kms_key_arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;execution_controls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ssm_controls&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;concurrent_execution_rate_percentage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;
      &lt;span class="nx"&gt;error_percentage&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_config_configuration_recorder"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;role_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;recording_group&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;all_supported&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_config_delivery_channel"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;s3_bucket_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_config_configuration_recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_config_configuration_recorder_status"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_config_configuration_recorder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;is_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_config_delivery_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&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;h3&gt;
  
  
  &lt;code&gt;stackset.tf&lt;/code&gt;: deploy Config rule to all member accounts
&lt;/h3&gt;

&lt;p&gt;CloudFormation StackSets let you deploy the Config rule and remediation to every member account without logging into each one. The &lt;code&gt;AutomationAssumeRole&lt;/code&gt; in the template body references a role that must exist in each member account. The StackSet creates it via &lt;code&gt;CAPABILITY_NAMED_IAM&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudformation_stack_set"&lt;/span&gt; &lt;span class="s2"&gt;"config_s3_ssekms"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ConfigS3SSEKMSRule"&lt;/span&gt;
  &lt;span class="nx"&gt;permission_model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SERVICE_MANAGED"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Deploys AWS Config rule to enforce SSE-KMS on S3 buckets"&lt;/span&gt;

  &lt;span class="nx"&gt;auto_deployment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt;                          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="nx"&gt;retain_stacks_on_account_removal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;capabilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CAPABILITY_NAMED_IAM"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;template_body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;AWSTemplateFormatVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2010-09-09"&lt;/span&gt;
    &lt;span class="nx"&gt;Parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;KMSKeyArn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"String"&lt;/span&gt;
        &lt;span class="nx"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ARN of the KMS key for SSE-KMS"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;Resources&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;SSMRemediationRole&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::IAM::Role"&lt;/span&gt;
        &lt;span class="nx"&gt;Properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;RoleName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSMRemediationS3SSEKMSRole"&lt;/span&gt;
          &lt;span class="nx"&gt;AssumeRolePolicyDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
            &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
              &lt;span class="nx"&gt;Effect&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
              &lt;span class="nx"&gt;Principal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ssm.amazonaws.com"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="nx"&gt;Action&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sts:AssumeRole"&lt;/span&gt;
            &lt;span class="p"&gt;}]&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;Policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="nx"&gt;PolicyName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSMRemediationS3SSEKMSPolicy"&lt;/span&gt;
            &lt;span class="nx"&gt;PolicyDocument&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;
              &lt;span class="nx"&gt;Statement&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
                  &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"s3:PutEncryptionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"s3:GetEncryptionConfiguration"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                  &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="nx"&gt;Effect&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow"&lt;/span&gt;
                  &lt;span class="nx"&gt;Action&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"kms:DescribeKey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                  &lt;span class="nx"&gt;Resource&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KMSKeyArn"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
              &lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;S3SSEKMSConfigRule&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::Config::ConfigRule"&lt;/span&gt;
        &lt;span class="nx"&gt;Properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ConfigRuleName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-bucket-server-side-encryption-enabled"&lt;/span&gt;
          &lt;span class="nx"&gt;Source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;Owner&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS"&lt;/span&gt;
            &lt;span class="nx"&gt;SourceIdentifier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;S3SSEKMSRemediation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;Type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::Config::RemediationConfiguration"&lt;/span&gt;
        &lt;span class="nx"&gt;DependsOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"S3SSEKMSConfigRule"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nx"&gt;Properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;ConfigRuleName&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-bucket-server-side-encryption-enabled"&lt;/span&gt;
          &lt;span class="nx"&gt;TargetType&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SSM_DOCUMENT"&lt;/span&gt;
          &lt;span class="nx"&gt;TargetId&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS-EnableS3BucketEncryption"&lt;/span&gt;
          &lt;span class="nx"&gt;Automatic&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="nx"&gt;MaximumAutomaticAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
          &lt;span class="nx"&gt;RetryAttemptSeconds&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
          &lt;span class="nx"&gt;ResourceType&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AWS::S3::Bucket"&lt;/span&gt;
          &lt;span class="nx"&gt;Parameters&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;AutomationAssumeRole&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;StaticValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nx"&gt;Values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
                  &lt;span class="s2"&gt;"Fn::GetAtt"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SSMRemediationRole"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Arn"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;}]&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;BucketName&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ResourceValue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RESOURCE_ID"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;SSEAlgorithm&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StaticValue&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws:kms"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nx"&gt;KMSMasterKeyID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;StaticValue&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;Ref&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KMSKeyArn"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;parameter_key&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"KMSKeyArn"&lt;/span&gt;
    &lt;span class="nx"&gt;parameter_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kms_key_arn&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudformation_stack_set_instance"&lt;/span&gt; &lt;span class="s2"&gt;"config_s3_ssekms_org"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;stack_set_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudformation_stack_set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config_s3_ssekms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;deployment_targets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;organizational_unit_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization_root_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;operation_preferences&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;failure_tolerance_percentage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="nx"&gt;max_concurrent_percentage&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&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;code&gt;auto_deployment&lt;/code&gt; enabled means that any new account added to the organization gets the Config rule and SSM role deployed automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happens after deployment?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;New bucket without SSE-KMS&lt;/strong&gt; -&amp;gt; &lt;code&gt;s3:CreateBucket&lt;/code&gt; hits the SCP condition -&amp;gt; &lt;code&gt;AccessDenied&lt;/code&gt;. The bucket is never created.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New bucket with SSE-KMS&lt;/strong&gt; -&amp;gt; passes the SCP condition -&amp;gt; created normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existing bucket without SSE-KMS&lt;/strong&gt; -&amp;gt; Config evaluates it as non-compliant -&amp;gt; SSM Automation triggers using &lt;code&gt;SSMRemediationS3SSEKMSRole&lt;/code&gt; -&amp;gt; &lt;code&gt;AWS-EnableS3BucketEncryption&lt;/code&gt; calls &lt;code&gt;s3:PutEncryptionConfiguration&lt;/code&gt; -&amp;gt; bucket becomes compliant. This happens within minutes of the rule being active.&lt;/p&gt;




&lt;h2&gt;
  
  
  Validating the deployment
&lt;/h2&gt;

&lt;p&gt;Three checks to run after &lt;code&gt;terraform apply&lt;/code&gt; completes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 1: SCP is blocking unencrypted bucket creation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From any member account (not the management account, which is excluded from SCPs):&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;# This should fail with AccessDenied&lt;/span&gt;
aws s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; test-no-encryption-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="c"&gt;# Expected output:&lt;/span&gt;
&lt;span class="c"&gt;# An error occurred (AccessDenied) when calling the CreateBucket operation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# This should succeed&lt;/span&gt;
aws s3api create-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; test-with-encryption-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--create-bucket-configuration&lt;/span&gt; &lt;span class="nv"&gt;LocationConstraint&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--object-lock-enabled-for-bucket&lt;/span&gt;

aws s3api put-bucket-encryption &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; test-with-encryption-&amp;lt;timestamp&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--server-side-encryption-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms",
        "KMSMasterKeyID": "&amp;lt;your-kms-key-arn&amp;gt;"
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 2: Config rule is active and evaluating&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;aws configservice describe-config-rules &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config-rule-names&lt;/span&gt; s3-bucket-server-side-encryption-enabled &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'ConfigRules[0].ConfigRuleState'&lt;/span&gt;
&lt;span class="c"&gt;# Expected: "ACTIVE"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Check 3: compliance status for existing buckets&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;aws configservice get-compliance-details-by-config-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config-rule-name&lt;/span&gt; s3-bucket-server-side-encryption-enabled &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compliance-types&lt;/span&gt; NON_COMPLIANT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'EvaluationResults[*].EvaluationResultIdentifier.EvaluationResultQualifier.ResourceId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Non-compliant buckets appearing here mean the SSM Automation has not run yet. Wait a few minutes and run the query again. To trigger remediation immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws configservice start-remediation-execution &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config-rule-name&lt;/span&gt; s3-bucket-server-side-encryption-enabled &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-keys&lt;/span&gt; &lt;span class="s1"&gt;'[{"resourceType":"AWS::S3::Bucket","resourceId":"&amp;lt;bucket-name&amp;gt;"}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  A note on the KMS key
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;KMSMasterKeyID&lt;/code&gt; parameter accepts either a key ARN or an alias. In a multi-account setup, you have two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a single centralized KMS key in the management account and share it via key policy with member accounts&lt;/li&gt;
&lt;li&gt;Create one KMS key per account and pass the correct ARN per account in the StackSet parameter overrides&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A centralized key with cross-account access is simpler to manage. If data sovereignty or account isolation is a concern, per-account keys give you finer control.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost considerations
&lt;/h2&gt;

&lt;p&gt;AWS Config charges per configuration item recorded. Every time a resource changes state, Config records it. With many accounts and many buckets, the total is worth knowing before you deploy.&lt;/p&gt;

&lt;p&gt;Current pricing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config rule evaluations&lt;/strong&gt;: $0.001 per evaluation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration items recorded&lt;/strong&gt;: $0.003 per item in the first 100K per month, per region&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: 50 accounts × 20 buckets × 10 configuration changes per bucket per month = 10,000 configuration items, around $30/month across the organization. The number grows with bucket churn, so if your environment creates and deletes buckets frequently, pull a Config cost report after the first month.&lt;/p&gt;

&lt;p&gt;The SCP itself has no cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why not the other options?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option 1&lt;/strong&gt; (CloudFormation StackSets + Config only, no SCP) covers existing buckets but does nothing to prevent new ones from being created without encryption. You are always reacting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 2&lt;/strong&gt; (Security Hub + EventBridge + Lambda) works, but you are now maintaining Lambda code, IAM roles for that Lambda, and EventBridge rules per account. More moving parts, more things that can break silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 3&lt;/strong&gt; (bucket policies via StackSets + Drift Detection) requires that the bucket already exists before you can apply a policy to it. It does not prevent the creation of unencrypted buckets. It adds a policy after the fact. Drift Detection is manual.&lt;/p&gt;

&lt;p&gt;The SCP + Config combination uses two managed AWS mechanisms, no custom code, and applies automatically to new accounts.&lt;/p&gt;

&lt;p&gt;See you next time, right? Bye.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>security</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How I would put an SLA operations portal on AWS (and what I weighed along the way)</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Tue, 02 Jun 2026 20:30:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/como-eu-colocaria-um-portal-de-operacoes-de-sla-na-aws-e-o-que-pesei-no-caminho-4clc</link>
      <guid>https://dev.to/aws-builders/como-eu-colocaria-um-portal-de-operacoes-de-sla-na-aws-e-o-que-pesei-no-caminho-4clc</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I maintain an internal portal that tracks SLA across several operational queues in Jira. It reads tickets through the Jira API, hours through the Tempo API, calculates a health score per team, and shows everything in a dashboard. Today it runs as a simple Flask server, with an in memory cache and a few JSON files to keep history.&lt;/p&gt;

&lt;p&gt;It works. But "runs on my machine" is not architecture. So I sat down to think: if I needed to ship this for real, with people depending on it, what would it look like on AWS?&lt;/p&gt;

&lt;p&gt;This post is that exercise. It is not a step by step Terraform tutorial. It is more about the decisions and the trade-offs. I will try to be honest about what makes sense and what would be overkill.&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%2Fbist637cvlyg5frwba5z.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%2Fbist637cvlyg5frwba5z.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Portal home screen: consolidated health score and per team.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What the portal does today
&lt;/h2&gt;

&lt;p&gt;Before throwing AWS on top of it, it helps to understand the parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: a Flask server in Python. It exposes a few REST endpoints (&lt;code&gt;/api/queues/:id/data&lt;/code&gt;, &lt;code&gt;/api/queues/overview&lt;/code&gt;, &lt;code&gt;/api/trends&lt;/code&gt;, and so on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: a single HTML file with a fair amount of JavaScript and Chart.js. No framework, no build step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External integrations&lt;/strong&gt;: the Jira REST API and the Tempo API. That is where all the data comes from.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache&lt;/strong&gt;: an in memory dictionary with a 5 minute TTL. The first call to a large queue (like Service Desk, with around 2,500 tickets) takes about 30 seconds; the next ones come back instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt;: two JSON files. One is the queue registry (configuration), the other keeps daily snapshots of the health score to build trend charts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily job&lt;/strong&gt;: once a day it takes a "snapshot" of all queues and saves it to history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets&lt;/strong&gt;: Jira and Tempo tokens, today in environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that it is a small system. That matters, because the temptation to throw ten AWS services at a project this size is strong and almost always wrong.&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%2Fphf0ahx0uebq6lt5vdwk.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%2Fphf0ahx0uebq6lt5vdwk.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Operations page for one queue: first response and resolution SLA, multidimensional health score, and distribution by analyst.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The AWS design
&lt;/h2&gt;

&lt;p&gt;I will split it by responsibility.&lt;/p&gt;
&lt;h3&gt;
  
  
  Where the backend runs
&lt;/h3&gt;

&lt;p&gt;The first question is: Lambda or container?&lt;/p&gt;

&lt;p&gt;The backend has one annoying detail. The call that fetches all tickets from a large queue can take more than 30 seconds because of Jira API pagination. Lambda has a maximum timeout of 15 minutes, so it would fit, but 30 seconds of cold start plus a synchronous call that size makes me uncomfortable for a UI. Nobody likes clicking and waiting half a minute.&lt;/p&gt;

&lt;p&gt;So I went with &lt;strong&gt;ECS Fargate&lt;/strong&gt;. A container running Flask (behind Gunicorn, not the Flask dev server), running as a service. I do not have to manage EC2, I pay for what I use, and I do not have the cold start dance for the heavier requests.&lt;/p&gt;

&lt;p&gt;I put an &lt;strong&gt;Application Load Balancer&lt;/strong&gt; in front. It handles HTTPS (with an ACM certificate) and distributes across the containers. If usage grows one day, I turn on Fargate auto scaling by CPU. Today, with the current team, one or two tasks are enough.&lt;/p&gt;

&lt;p&gt;One alternative I considered was App Runner. It is simpler than plain Fargate and it abstracts away the load balancer. For a project this size, App Runner would probably be enough and would give me less to configure. I stayed with Fargate because I know it better and I want control over the networking, but if I were in a hurry, App Runner would be a defensible choice.&lt;/p&gt;
&lt;h3&gt;
  
  
  The frontend
&lt;/h3&gt;

&lt;p&gt;The frontend is static HTML. It does not need a server for that.&lt;/p&gt;

&lt;p&gt;I drop the file in an &lt;strong&gt;S3&lt;/strong&gt; bucket and put &lt;strong&gt;CloudFront&lt;/strong&gt; in front. CloudFront caches the HTML close to the user and serves it over HTTPS. Flask stops having the job of serving the HTML and becomes only an API.&lt;/p&gt;

&lt;p&gt;A practical detail: since the frontend calls the API at &lt;code&gt;/api/...&lt;/code&gt;, I configure CloudFront with two origins. A request that hits &lt;code&gt;/api/*&lt;/code&gt; goes to the Load Balancer (the backend); everything else comes from S3. That way the user only knows one domain and I do not have to worry about CORS.&lt;/p&gt;
&lt;h3&gt;
  
  
  Persistence
&lt;/h3&gt;

&lt;p&gt;This is where I had to hold back the most so I would not overcomplicate things.&lt;/p&gt;

&lt;p&gt;The two JSON files have different natures:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue registry&lt;/strong&gt; (&lt;code&gt;queue_registry.json&lt;/code&gt;): it is configuration. It rarely changes, and by hand. This can stay a file, just in an &lt;strong&gt;S3&lt;/strong&gt; bucket instead of local disk. The backend reads it at startup (or with a short cache). I do not need a database for this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snapshot history&lt;/strong&gt; (&lt;code&gt;trends.json&lt;/code&gt;): this grows every day and I run queries like "give me the last 30 days of this queue." A single JSON file that keeps growing is a bad idea in the medium term, because every time I read the whole file just to grab a slice. Here &lt;strong&gt;DynamoDB&lt;/strong&gt; fits well. Each snapshot becomes an item, with the date as part of the key. I can fetch a date range without reading the entire history. And it is serverless, so I do not have a database instance to babysit on a small project.&lt;/p&gt;

&lt;p&gt;I noticed I could have gone with RDS, but I have no complex relationships and no JOINs. It would mean paying for something I do not use. DynamoDB with a simple access pattern (queue key plus date) does the job.&lt;/p&gt;
&lt;h3&gt;
  
  
  The cache
&lt;/h3&gt;

&lt;p&gt;Today the cache lives in process memory. It works because it is a single process. The moment I have two tasks on Fargate, each one has its own cache, and that is where the inconsistency starts. One user gets data from 1 minute ago, another from 4 minutes ago.&lt;/p&gt;

&lt;p&gt;The standard answer for this is &lt;strong&gt;ElastiCache (Redis)&lt;/strong&gt;. A cache shared across the tasks, with a TTL. The tasks write to and read from the same place.&lt;/p&gt;

&lt;p&gt;But, and here is where I would be honest in a real article, for this volume I might not even stand up Redis on day one. Keeping the cache in memory with two tasks means that, in the worst case, the data can be 5 minutes "out of phase" between one task and another. For an SLA dashboard that refreshes every 5 minutes anyway, that is tolerable. I would add Redis when the number of tasks grows or when the cost of re fetching from Jira starts to hurt. It is one of those things you can put off without shame.&lt;/p&gt;
&lt;h3&gt;
  
  
  The daily job
&lt;/h3&gt;

&lt;p&gt;The daily snapshot is a cron today. On AWS that becomes &lt;strong&gt;EventBridge Scheduler&lt;/strong&gt; firing once a day.&lt;/p&gt;

&lt;p&gt;What does it fire? Here Lambda makes sense. The job does not have the UI latency problem. It runs in the background, it can take the 30 to 40 seconds fetching all the queues, and nobody is waiting on a screen. A Lambda function that takes the snapshot and writes to DynamoDB. Cheap, with no server running all day just to run once.&lt;/p&gt;
&lt;h3&gt;
  
  
  Secrets
&lt;/h3&gt;

&lt;p&gt;The Jira and Tempo tokens cannot sit in a plain text environment variable in a serious setup. They go to &lt;strong&gt;Secrets Manager&lt;/strong&gt;. The container and the Lambda pull the tokens at runtime through an IAM role. Bonus: Secrets Manager does rotation if we configure it, although a third party API token does not always support automatic rotation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Logs and the minimum of observability
&lt;/h3&gt;

&lt;p&gt;Everything sends logs to &lt;strong&gt;CloudWatch Logs&lt;/strong&gt;. That comes almost for free with Fargate and Lambda.&lt;/p&gt;

&lt;p&gt;I would set at least two alarms in &lt;strong&gt;CloudWatch&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the 5xx error rate on the Load Balancer rises, I want to know.&lt;/li&gt;
&lt;li&gt;If the daily job fails (the Lambda errors out), I want to know, because otherwise the trend chart gets a gap and I only find out weeks later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am not going to build an elaborate observability dashboard for a project this size. The CloudWatch logs and two alarms cover what matters.&lt;/p&gt;
&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;In the AWS version, the user would reach the portal through a domain like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://d1a2b3c4d5e6f7.cloudfront.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(that is the domain CloudFront generates automatically; in production I would point a CNAME like &lt;code&gt;cldops.mycompany.com&lt;/code&gt; to it through Route 53)&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User reaches &lt;code&gt;https://d1a2b3c4d5e6f7.cloudfront.net&lt;/code&gt;, hits CloudFront.&lt;/li&gt;
&lt;li&gt;CloudFront serves the HTML from S3.&lt;/li&gt;
&lt;li&gt;The JavaScript calls &lt;code&gt;/api/...&lt;/code&gt;, CloudFront routes it to the Load Balancer, then to Fargate (Flask/Gunicorn).&lt;/li&gt;
&lt;li&gt;Flask checks the cache (memory or Redis), and if needed, fetches from Jira/Tempo.&lt;/li&gt;
&lt;li&gt;Queue configuration comes from a JSON in S3; history comes from DynamoDB.&lt;/li&gt;
&lt;li&gt;Once a day, EventBridge fires a Lambda that takes the snapshot and writes to DynamoDB.&lt;/li&gt;
&lt;li&gt;Tokens live in Secrets Manager; logs and alarms in CloudWatch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;As a box diagram, it looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                          ┌─────────────────┐
   user ────HTTPS───────► │   CloudFront     │
                          │ d1a2b3c4d5e6f7   │
                          │ .cloudfront.net  │
                          └────────┬─────────┘
                          /            \
                   /api/* │             │ rest
                          ▼             ▼
                  ┌──────────────┐   ┌──────────┐
                  │     ALB      │   │    S3     │
                  └──────┬───────┘   │ (HTML)    │
                         ▼           └──────────┘
                  ┌──────────────┐
                  │ ECS Fargate  │──► Jira API
                  │ Flask/Gunic. │──► Tempo API
                  └──────┬───────┘
                         │
              ┌──────────┼──────────┐
              ▼          ▼          ▼
        ┌─────────┐ ┌─────────┐ ┌──────────┐
        │DynamoDB │ │  S3     │ │ Secrets  │
        │(trends) │ │(config) │ │ Manager  │
        └─────────┘ └─────────┘ └──────────┘

   EventBridge (1x/day) ──► Lambda (snapshot) ──► DynamoDB
&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%2F5swtcbbz6fj5juuj5xcp.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%2F5swtcbbz6fj5juuj5xcp.png" alt=" "&gt;&lt;/a&gt;&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%2Fo8dodcin8xpvui8guwvg.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%2Fo8dodcin8xpvui8guwvg.png" alt=" "&gt;&lt;/a&gt;&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%2Fmt3p4tkgl3pfxwsk5mi4.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%2Fmt3p4tkgl3pfxwsk5mi4.png" alt=" "&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The cross queue comparison: you can see at a glance which queue is healthy and which one needs attention.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How this becomes IaC
&lt;/h2&gt;

&lt;p&gt;A diagram is nice, but it does not deploy anything. I would write this in Terraform so I do not end up clicking around the console and forgetting what I did. I am not going to paste the whole module here (it would be huge), but I can show the skeleton so you get a sense of the size.&lt;/p&gt;

&lt;p&gt;The file structure I would use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;infra/
├── main.tf            # providers and state backend
├── variables.tf       # region, project name, etc
├── network.tf         # VPC, subnets, security groups
├── ecs.tf             # cluster, task definition, service
├── alb.tf             # load balancer and target group
├── frontend.tf        # S3 bucket + CloudFront
├── data.tf            # DynamoDB + config bucket
├── scheduler.tf       # EventBridge + snapshot Lambda
├── secrets.tf         # Secrets Manager
└── outputs.tf         # CloudFront URL, etc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A piece of &lt;code&gt;frontend.tf&lt;/code&gt;, which is what generates that CloudFront domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bucket that holds the dashboard HTML&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"frontend"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-frontend"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_cloudfront_distribution"&lt;/span&gt; &lt;span class="s2"&gt;"portal"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;enabled&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;default_root_object&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"dashboard.html"&lt;/span&gt;

  &lt;span class="c1"&gt;# Origin 1: the static HTML in S3&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frontend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_regional_domain_name&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-frontend"&lt;/span&gt;
    &lt;span class="nx"&gt;origin_access_control_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudfront_origin_access_control&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frontend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Origin 2: the API on the load balancer&lt;/span&gt;
  &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dns_name&lt;/span&gt;
    &lt;span class="nx"&gt;origin_id&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alb-api"&lt;/span&gt;
    &lt;span class="nx"&gt;custom_origin_config&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;http_port&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
      &lt;span class="nx"&gt;https_port&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt;
      &lt;span class="nx"&gt;origin_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https-only"&lt;/span&gt;
      &lt;span class="nx"&gt;origin_ssl_protocols&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TLSv1.2"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Everything that is NOT /api/* comes from S3&lt;/span&gt;
  &lt;span class="nx"&gt;default_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target_origin_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-frontend"&lt;/span&gt;
    &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cached_methods&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cache_policy_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_cloudfront_cache_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optimized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# /api/* goes to the backend, no cache (dynamic data)&lt;/span&gt;
  &lt;span class="nx"&gt;ordered_cache_behavior&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;path_pattern&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/api/*"&lt;/span&gt;
    &lt;span class="nx"&gt;target_origin_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"alb-api"&lt;/span&gt;
    &lt;span class="nx"&gt;viewer_protocol_policy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"redirect-to-https"&lt;/span&gt;
    &lt;span class="nx"&gt;allowed_methods&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"OPTIONS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cached_methods&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"HEAD"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;cache_policy_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_cloudfront_cache_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;restrictions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;geo_restriction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;restriction_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"none"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;viewer_certificate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cloudfront_default_certificate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"portal_url"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://${aws_cloudfront_distribution.portal.domain_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the detail with the two &lt;code&gt;cache_behavior&lt;/code&gt; blocks: I let CloudFront cache the static HTML freely, but I mark &lt;code&gt;/api/*&lt;/code&gt; as no cache, because it is dynamic SLA data. If I cached the API at CloudFront, it would fight with the 5 minute cache that already exists in the backend and the data would go stale in ways that are hard to debug.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;outputs.tf&lt;/code&gt; prints the URL at the end of &lt;code&gt;terraform apply&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# after the apply:&lt;/span&gt;
&lt;span class="c1"&gt;# portal_url = "https://d1a2b3c4d5e6f7.cloudfront.net"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A snippet of &lt;code&gt;ecs.tf&lt;/code&gt; to give an idea of how the backend is declared:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_ecs_task_definition"&lt;/span&gt; &lt;span class="s2"&gt;"api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;family&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.project}-api"&lt;/span&gt;
  &lt;span class="nx"&gt;requires_compatibilities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;network_mode&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awsvpc"&lt;/span&gt;
  &lt;span class="nx"&gt;cpu&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;
  &lt;span class="nx"&gt;memory&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;
  &lt;span class="nx"&gt;execution_role_arn&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_execution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
  &lt;span class="nx"&gt;task_role_arn&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecs_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;container_definitions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"flask-api"&lt;/span&gt;
    &lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${aws_ecr_repository.api.repository_url}:latest"&lt;/span&gt;
    &lt;span class="nx"&gt;portMappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;containerPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="c1"&gt;# tokens come from Secrets Manager, not hardcoded&lt;/span&gt;
    &lt;span class="nx"&gt;secrets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"JIRA_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueFrom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jira&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TEMPO_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;valueFrom&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tempo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;logConfiguration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logDriver&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"awslogs"&lt;/span&gt;
      &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"awslogs-group"&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_cloudwatch_log_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
        &lt;span class="s2"&gt;"awslogs-region"&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
        &lt;span class="s2"&gt;"awslogs-stream-prefix"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"flask"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&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;This is not the full file. It is missing the &lt;code&gt;aws_ecs_service&lt;/code&gt;, the auto scaling, the IAM policies with the right permission for the secrets, the &lt;code&gt;network.tf&lt;/code&gt; with the VPC. But you can see it is not magic: it is declaring each box from the diagram and wiring one to the other by their IDs.&lt;/p&gt;

&lt;p&gt;If I were doing this for real, I probably would not even write all of it by hand. I would use a ready made community module for the VPC (the &lt;code&gt;terraform-aws-modules/vpc/aws&lt;/code&gt; is well tested) and spend my time on what is specific to the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I left out on purpose
&lt;/h2&gt;

&lt;p&gt;I think half the value of this kind of exercise is in what you do NOT include.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Kubernetes (EKS)&lt;/strong&gt;. For a single container, EKS would be bringing a tractor to go to the bakery.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No separate API Gateway&lt;/strong&gt;. The Load Balancer already handles the routing. API Gateway would make sense if I had several APIs, complex authorization, throttling per client. That is not the case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No data lake, no Athena, no Glue&lt;/strong&gt;. It is SLA history, not big data. DynamoDB handles it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi region&lt;/strong&gt;. It is an internal tool. If the region goes down for a few hours, the team looks at Jira directly. It is not worth the complexity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rough cost
&lt;/h2&gt;

&lt;p&gt;I am not going to pretend I know the exact number, because it depends a lot on usage. But the order of magnitude for an internal system this size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fargate: the biggest item, but with one or two small tasks it is manageable.&lt;/li&gt;
&lt;li&gt;S3 + CloudFront: cents for this amount of traffic.&lt;/li&gt;
&lt;li&gt;DynamoDB: on demand, paying per write/read, with one snapshot a day and occasional queries, it stays very low.&lt;/li&gt;
&lt;li&gt;Lambda + EventBridge: runs once a day, basically free.&lt;/li&gt;
&lt;li&gt;Secrets Manager: charges per secret per month, a few dollars.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The bulk of the cost is Fargate running all the time. If usage were very sporadic, then it would be worth reconsidering Lambda for the backend and accepting the cold start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration, if I were to do it for real
&lt;/h2&gt;

&lt;p&gt;I would not do everything at once. The order I would follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Containerize Flask and ship it to Fargate behind the Load Balancer. The system keeps using local JSON files inside the container (ephemeral, but it works to start).&lt;/li&gt;
&lt;li&gt;Move the frontend to S3 + CloudFront.&lt;/li&gt;
&lt;li&gt;Take the tokens out of the environment and put them in Secrets Manager.&lt;/li&gt;
&lt;li&gt;Migrate the snapshot history from JSON to DynamoDB. This is the step that actually changes code.&lt;/li&gt;
&lt;li&gt;Move the daily job to EventBridge + Lambda.&lt;/li&gt;
&lt;li&gt;Only then, if needed, stand up Redis on ElastiCache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each step leaves the system working. No big bang.&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%2Fse18vl14wgydyk9wlc1f.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%2Fse18vl14wgydyk9wlc1f.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The trends page uses the daily snapshots, which is exactly the data that would move to DynamoDB.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The point I wanted to get across is that good architecture is not the one that uses the most services. It is the one that uses enough for the problem you have. This portal is small and will probably stay small. Its AWS version reflects that: Fargate for the backend, S3/CloudFront for the front, DynamoDB for the history, Lambda for the job, and the rest is plumbing (Secrets Manager, CloudWatch, IAM).&lt;/p&gt;

&lt;p&gt;If it ever grows to dozens of teams and thousands of hits a day, I will revisit it. But designing for that hypothetical future now would mean spending time and money on a problem I do not have yet.&lt;/p&gt;

&lt;p&gt;If you have a similar little project, an internal tool that "runs on your machine," I recommend doing this exercise. You learn more by designing the production version of something small than by copying an architecture diagram from a big company.&lt;/p&gt;

&lt;p&gt;Comments and corrections are welcome. If you would have done something differently, tell me, especially on the cache part, which is where I was most unsure.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>python</category>
      <category>devops</category>
      <category>observability</category>
    </item>
    <item>
      <title>O AWS App Runner deixará de aceitar novos registros em 30 de abril. O Amazon RDS Custom para Oracle será descontinuado, entre outros serviços da AWS.</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Tue, 28 Apr 2026 15:50:34 +0000</pubDate>
      <link>https://dev.to/aws-builders/o-aws-app-runner-deixara-de-aceitar-novos-registros-em-30-de-abril-o-amazon-rds-custom-para-oracle-3naf</link>
      <guid>https://dev.to/aws-builders/o-aws-app-runner-deixara-de-aceitar-novos-registros-em-30-de-abril-o-amazon-rds-custom-para-oracle-3naf</guid>
      <description>&lt;p&gt;Nos últimos dias, a AWS anunciou uma série de mudanças no ciclo de vida de alguns serviços. À primeira vista, pode parecer só mais um daqueles comunicados técnicos que passam batido no meio de tanta novidade.&lt;/p&gt;

&lt;p&gt;Mas quando você olha com um pouco mais de atenção, dá pra perceber que não é somente descontinuação.&lt;/p&gt;

&lt;p&gt;Alguns serviços estão entrando em manutenção o que significa que deixam de aceitar novos clientes enquanto outros já têm data definida para deixar de existir.&lt;/p&gt;

&lt;p&gt;E dois casos ajudam a entender bem o que está acontecendo.&lt;/p&gt;

&lt;h3&gt;
  
  
  App Runner: continua funcionando, mas ficou pelo caminho
&lt;/h3&gt;

&lt;p&gt;O AWS App Runner entra em modo de manutenção a partir de 30 de abril de 2026. Isso quer dizer que novos clientes não conseguem mais utilizar o serviço, embora quem já esteja usando continue normalmente. Esse tipo de movimento é curioso pois o serviço não foi encerrado, ele simplesmente deixará de evoluir.&lt;/p&gt;

&lt;p&gt;Fica ali, funcionando, mas sem novas funcionalidades, sem avanço. Com o tempo, naturalmente, começa a perder espaço.&lt;/p&gt;

&lt;h3&gt;
  
  
  RDS Custom for Oracle: aqui o caminho já está definido
&lt;/h3&gt;

&lt;p&gt;No caso do RDS Custom for Oracle, a situação é mais direta.&lt;br&gt;
A AWS já anunciou que o serviço será encerrado em 31 de março de 2027. Depois disso, não será mais possível acessar instâncias, snapshots ou qualquer recurso relacionado.&lt;/p&gt;

&lt;p&gt;Até lá, o serviço continua operando normalmente. Mas a própria AWS já orienta que os clientes planejem a migração, com recomendação clara de saída para ambientes em EC2.&lt;/p&gt;

&lt;h3&gt;
  
  
  O papel que o RDS Custom ocupava na AWS
&lt;/h3&gt;

&lt;p&gt;Esse serviço sempre existiu para resolver um problema específico.&lt;/p&gt;

&lt;p&gt;Ele permitia usar um modelo gerenciado, como o RDS, mas sem abrir mão do acesso ao sistema operacional e ao próprio banco.&lt;br&gt;
Isso fazia sentido para aplicações legadas ou cenários mais sensíveis, onde não dava para trabalhar dentro das limitações do modelo padrão.&lt;/p&gt;

&lt;p&gt;Ao mesmo tempo, criava uma situação um pouco desconfortável:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Não era totalmente gerenciado&lt;/li&gt;
&lt;li&gt;Nem totalmente controlado pelo cliente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;E isso costuma trazer mais complexidade do que parece no início.&lt;/p&gt;

&lt;h3&gt;
  
  
  O que esses dois casos têm em comum?
&lt;/h3&gt;

&lt;p&gt;Quando você coloca App Runner e RDS Custom lado a lado, começa a aparecer um padrão. São serviços que ocupam um espaço intermediário.&lt;/p&gt;

&lt;p&gt;De um lado, o App Runner simplifica bastante a operação. Do outro, o RDS Custom dá liberdade além do padrão gerenciado.&lt;br&gt;
Mas nenhum dos dois se encaixa exatamente no modelo mais comum que a AWS vem reforçando. E, aos poucos, esse tipo de solução vai sendo deixado de lado.&lt;/p&gt;

&lt;h3&gt;
  
  
  Um movimento que aparece com frequência
&lt;/h3&gt;

&lt;p&gt;Esse tipo de ajuste não é novo.&lt;/p&gt;

&lt;p&gt;Plataformas grandes, com muitos serviços, precisam revisar o portfólio de tempos em tempos. Nem tudo continua fazendo sentido conforme o uso real evolui.&lt;/p&gt;

&lt;p&gt;O que chama a atenção aqui é a direção. Cada vez menos espaço para soluções “no meio do caminho”.&lt;/p&gt;

&lt;p&gt;Ou você segue um modelo mais gerenciado, com menos preocupação operacional. Ou assume o controle completo do ambiente.&lt;/p&gt;

&lt;h3&gt;
  
  
  E o impacto disso no dia a dia
&lt;/h3&gt;

&lt;p&gt;Esse tipo de mudança costuma se manifestar de forma mais concreta em outro momento.&lt;/p&gt;

&lt;p&gt;Quando um ambiente já está rodando há algum tempo. Uma escolha feita lá atrás começa a ser revisitada para saber se é a melhor forma de operar. E aí surgem coisas que não estavam no radar inicial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;necessidade de migração&lt;/li&gt;
&lt;li&gt;ajustes de arquitetura&lt;/li&gt;
&lt;li&gt;revisão de operação&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  O que vemos aqui não é somente descontinuação
&lt;/h3&gt;

&lt;p&gt;Esses anúncios acabam sendo mais úteis quando vistos como sinal pois nos mostram o que continua sendo incentivado dentro da plataforma e o que está ficando para trás.&lt;/p&gt;

&lt;p&gt;E isso acaba influenciando bastante as decisões que estão sendo tomadas agora, principalmente quando alguém está desenhando algo novo.&lt;/p&gt;

&lt;p&gt;Porque, mais do que saber o que existe hoje, o que realmente pesa é entender o que ainda vai fazer sentido amanhã.&lt;/p&gt;

&lt;p&gt;Referência: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability/" rel="noopener noreferrer"&gt;https://aws.amazon.com/about-aws/whats-new/2026/03/aws-service-availability/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all folks.&lt;/p&gt;

</description>
      <category>apprunner</category>
      <category>customoracle</category>
      <category>rds</category>
      <category>expiration</category>
    </item>
    <item>
      <title>A AWS lançou um ambiente de testes onde você pode experimentar o Aurora DSQL imediatamente, sem precisar fazer login.</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Mon, 09 Mar 2026 13:42:52 +0000</pubDate>
      <link>https://dev.to/aws-builders/a-aws-lancou-um-ambiente-de-testes-onde-voce-pode-experimentar-o-aurora-dsql-imediatamente-sem-4mi0</link>
      <guid>https://dev.to/aws-builders/a-aws-lancou-um-ambiente-de-testes-onde-voce-pode-experimentar-o-aurora-dsql-imediatamente-sem-4mi0</guid>
      <description>&lt;p&gt;A Amazon Web Services (AWS) anunciou o lançamento de um ambiente de testes onde você pode experimentar seu serviço de banco de dados, o Amazon Aurora DSQL .&lt;/p&gt;

&lt;p&gt;O Amazon Aurora DSQL é um banco de dados distribuído de grande escala com capacidade de escalabilidade praticamente ilimitada e compatibilidade com PostgreSQL. Foi lançado oficialmente em maio de 2025.&lt;/p&gt;

&lt;p&gt;Referência: A AWS lança oficialmente o "Amazon Aurora DSQL", um banco de dados distribuído em larga escala compatível com o PostgreSQL e que escala praticamente sem limites.&lt;/p&gt;

&lt;p&gt;É descrito como um banco de dados compatível com PostgreSQL que não apenas atinge alta escalabilidade por meio de processamento distribuído e alta disponibilidade por meio de configurações redundantes usando múltiplas zonas e regiões de disponibilidade, mas também supera a alta latência que era considerada uma fraqueza ao se obter forte consistência por meio do processamento de transações em um banco de dados amplamente distribuído, como em múltiplas regiões, e combina alta velocidade com baixa latência.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifique facilmente as incompatibilidades entre o Amazon DSQL e o PostgreSQL.
&lt;/h3&gt;

&lt;p&gt;No entanto, devido às diferenças de arquitetura resultantes de sua implementação como um banco de dados distribuído em larga escala, o Aurora DSQL não é totalmente compatível com o PostgreSQL, e existem incompatibilidades em áreas como definição de esquema de banco de dados, criação de índices e processamento de transações .&lt;/p&gt;

&lt;p&gt;Este ambiente de testes foi criado para permitir que os desenvolvedores verifiquem facilmente essas incompatibilidades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Não é necessário cadastro, você pode experimentar a amostra imediatamente.
&lt;/h3&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%2Fqj61k0hcp4mutljwku0j.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%2Fqj61k0hcp4mutljwku0j.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
(&lt;em&gt;Imagem do &lt;a href="https://aws.amazon.com/rds/aurora/dsql/" rel="noopener noreferrer"&gt;https://aws.amazon.com/rds/aurora/dsql/&lt;/a&gt;&lt;/em&gt;)&lt;/p&gt;

&lt;p&gt;A tela inicial é mostrada abaixo. Você pode optar por experimentar um banco de dados vazio ou um dos dois bancos de dados de exemplo fornecidos.&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%2Fyoa6unxxz6nf49lvsn1p.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%2Fyoa6unxxz6nf49lvsn1p.png" alt=" " width="521" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Se você selecionar a terceira opção, "Chinook", verá que as configurações de esquema e exemplos de instruções SQL para inserção de dados são fornecidos.&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%2Fmfi8v3707i22j2ehcc3f.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%2Fmfi8v3707i22j2ehcc3f.png" alt=" " width="545" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Após executar este comando, selecione "REPL" na tela de consulta e execute uma instrução SQL (por exemplo, select * from artists;), os resultados serão exibidos imediatamente.&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%2Fu0ix4b6bmw90hiebfmhz.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%2Fu0ix4b6bmw90hiebfmhz.png" alt=" " width="545" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all folks.&lt;/p&gt;

</description>
      <category>aurora</category>
      <category>dsql</category>
      <category>database</category>
      <category>aws</category>
    </item>
    <item>
      <title>Solving the problem of detecting deviations in multi-account environments using AWS Config and EventBridge</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Tue, 11 Nov 2025 17:56:58 +0000</pubDate>
      <link>https://dev.to/aws-builders/solving-the-problem-of-detecting-deviations-in-multi-account-environments-using-aws-config-and-33lo</link>
      <guid>https://dev.to/aws-builders/solving-the-problem-of-detecting-deviations-in-multi-account-environments-using-aws-config-and-33lo</guid>
      <description>&lt;p&gt;Hello guys! I am back after some months working on this project. So, let's go!&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem
&lt;/h2&gt;

&lt;p&gt;Working with a client that manages several different AWS accounts, I encountered a critical situation: resources being manually modified outside of CloudFormation in production. In practice, this meant that what was in the code no longer reflected the reality of the infrastructure.&lt;/p&gt;

&lt;p&gt;What is not always evident is that this “configuration drift” can cause:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rollback failures&lt;/li&gt;
&lt;li&gt;Inconsistencies between environments&lt;/li&gt;
&lt;li&gt;Compliance issues&lt;/li&gt;
&lt;li&gt;Difficulty tracking changes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Centralized drift detection system
&lt;/h2&gt;

&lt;p&gt;I created an architecture that monitors drift in real-time across multiple AWS accounts, utilizing AWS Config, EventBridge, and SNS for immediate notification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Let's go to the implementation
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Account Hub (Centralized Account)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# hub-account-template.yaml&lt;/span&gt;
&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Drift&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Detection&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Hub&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Central&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Monitoring&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Account'&lt;/span&gt;

&lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;SpokeAccountIds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CommaDelimitedList&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List of spoke account IDs to monitor&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123456789012,987654321098"&lt;/span&gt;

  &lt;span class="na"&gt;NotificationEmail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Email for drift notifications&lt;/span&gt;

&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Central Event Bus to receive events from all accounts&lt;/span&gt;
  &lt;span class="na"&gt;CentralEventBus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Events::EventBus&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;DriftDetectionCentralBus&lt;/span&gt;

  &lt;span class="c1"&gt;# Permission for spoke accounts to send events&lt;/span&gt;
  &lt;span class="na"&gt;EventBusPermission&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Events::Permission&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;StatementId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AllowSpokeAccounts&lt;/span&gt;
      &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CentralEventBus&lt;/span&gt;
      &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
      &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events:PutEvents&lt;/span&gt;
      &lt;span class="na"&gt;Condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;StringEquals&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws:PrincipalOrgID"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${AWS::AccountId}"&lt;/span&gt;

  &lt;span class="c1"&gt;# DynamoDB for storing drift history&lt;/span&gt;
  &lt;span class="na"&gt;DriftHistoryTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drift-detection-history&lt;/span&gt;
      &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
      &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stackId&lt;/span&gt;
          &lt;span class="na"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp&lt;/span&gt;
          &lt;span class="na"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;N&lt;/span&gt;
      &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stackId&lt;/span&gt;
          &lt;span class="na"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HASH&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;timestamp&lt;/span&gt;
          &lt;span class="na"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RANGE&lt;/span&gt;
      &lt;span class="na"&gt;StreamSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;StreamViewType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NEW_AND_OLD_IMAGES&lt;/span&gt;
      &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ttl&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Lambda for processing drift events&lt;/span&gt;
  &lt;span class="na"&gt;DriftProcessorFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drift-processor&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3.11&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.handler&lt;/span&gt;
      &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&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;Variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DriftHistoryTable&lt;/span&gt;
          &lt;span class="na"&gt;SNS_TOPIC_ARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;AlertTopic&lt;/span&gt;
      &lt;span class="na"&gt;Role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftProcessorRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;Code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ZipFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;import json&lt;/span&gt;
          &lt;span class="s"&gt;import boto3&lt;/span&gt;
          &lt;span class="s"&gt;import os&lt;/span&gt;
          &lt;span class="s"&gt;from datetime import datetime&lt;/span&gt;
          &lt;span class="s"&gt;from decimal import Decimal&lt;/span&gt;

          &lt;span class="s"&gt;dynamodb = boto3.resource('dynamodb')&lt;/span&gt;
          &lt;span class="s"&gt;sns = boto3.client('sns')&lt;/span&gt;
          &lt;span class="s"&gt;table = dynamodb.Table(os.environ['TABLE_NAME'])&lt;/span&gt;

          &lt;span class="s"&gt;def handler(event, context):&lt;/span&gt;
              &lt;span class="s"&gt;print(f"Received event: {json.dumps(event)}")&lt;/span&gt;

              &lt;span class="s"&gt;detail = event['detail']&lt;/span&gt;
              &lt;span class="s"&gt;account_id = event['account']&lt;/span&gt;
              &lt;span class="s"&gt;region = event['region']&lt;/span&gt;

              &lt;span class="s"&gt;# Processar drift detection&lt;/span&gt;
              &lt;span class="s"&gt;stack_id = detail['stack-id']&lt;/span&gt;
              &lt;span class="s"&gt;stack_name = detail['stack-name']&lt;/span&gt;
              &lt;span class="s"&gt;drift_status = detail['drift-status']&lt;/span&gt;

              &lt;span class="s"&gt;# Timestamp for sorting&lt;/span&gt;
              &lt;span class="s"&gt;timestamp = Decimal(str(datetime.utcnow().timestamp()))&lt;/span&gt;

              &lt;span class="s"&gt;# Store in DynamoDB&lt;/span&gt;
              &lt;span class="s"&gt;item = {&lt;/span&gt;
                  &lt;span class="s"&gt;'stackId': stack_id,&lt;/span&gt;
                  &lt;span class="s"&gt;'timestamp': timestamp,&lt;/span&gt;
                  &lt;span class="s"&gt;'accountId': account_id,&lt;/span&gt;
                  &lt;span class="s"&gt;'region': region,&lt;/span&gt;
                  &lt;span class="s"&gt;'stackName': stack_name,&lt;/span&gt;
                  &lt;span class="s"&gt;'driftStatus': drift_status,&lt;/span&gt;
                  &lt;span class="s"&gt;'driftedResources': detail.get('drifted-resources', []),&lt;/span&gt;
                  &lt;span class="s"&gt;'ttl': int(timestamp + 2592000)  # 30 days TTL&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;

              &lt;span class="s"&gt;table.put_item(Item=item)&lt;/span&gt;

              &lt;span class="s"&gt;# If there is drift, send alert&lt;/span&gt;
              &lt;span class="s"&gt;if drift_status == 'DRIFTED':&lt;/span&gt;
                  &lt;span class="s"&gt;message = f"""&lt;/span&gt;
                  &lt;span class="s"&gt;⚠️ DRIFT DETECTED&lt;/span&gt;

                  &lt;span class="s"&gt;Account: {account_id}&lt;/span&gt;
                  &lt;span class="s"&gt;Region: {region}&lt;/span&gt;
                  &lt;span class="s"&gt;Stack: {stack_name}&lt;/span&gt;

                  &lt;span class="s"&gt;Drifted Resources:&lt;/span&gt;
                  &lt;span class="s"&gt;{json.dumps(detail.get('drifted-resources', []), indent=2)}&lt;/span&gt;

                  &lt;span class="s"&gt;Action Required: Review and update CloudFormation template or revert manual changes.&lt;/span&gt;
                  &lt;span class="s"&gt;"""&lt;/span&gt;

                  &lt;span class="s"&gt;sns.publish(&lt;/span&gt;
                      &lt;span class="s"&gt;TopicArn=os.environ['SNS_TOPIC_ARN'],&lt;/span&gt;
                      &lt;span class="s"&gt;Subject=f'🚨 Drift Detected: {stack_name}',&lt;/span&gt;
                      &lt;span class="s"&gt;Message=message&lt;/span&gt;
                  &lt;span class="s"&gt;)&lt;/span&gt;

              &lt;span class="s"&gt;return {'statusCode': 200}&lt;/span&gt;

  &lt;span class="na"&gt;DriftProcessorRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::IAM::Role&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lambda.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sts:AssumeRole&lt;/span&gt;
      &lt;span class="na"&gt;ManagedPolicyArns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DriftProcessorPolicy&lt;/span&gt;
          &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:Query&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftHistoryTable.Arn&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;sns:Publish&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;AlertTopic&lt;/span&gt;

  &lt;span class="c1"&gt;# EventBridge Rule to capture drift events&lt;/span&gt;
  &lt;span class="na"&gt;DriftDetectionRule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Events::Rule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;capture-drift-events&lt;/span&gt;
      &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;CentralEventBus&lt;/span&gt;
      &lt;span class="na"&gt;EventPattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;custom.drift-detection&lt;/span&gt;
        &lt;span class="na"&gt;detail-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Stack Drift Detected&lt;/span&gt;
      &lt;span class="na"&gt;State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENABLED&lt;/span&gt;
      &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftProcessorFunction.Arn&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;drift-processor&lt;/span&gt;

  &lt;span class="c1"&gt;# Permission for EventBridge to invoke Lambda&lt;/span&gt;
  &lt;span class="na"&gt;LambdaInvokePermission&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Permission&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DriftProcessorFunction&lt;/span&gt;
      &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lambda:InvokeFunction&lt;/span&gt;
      &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events.amazonaws.com&lt;/span&gt;
      &lt;span class="na"&gt;SourceArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftDetectionRule.Arn&lt;/span&gt;

  &lt;span class="c1"&gt;# SNS Topic for alerts&lt;/span&gt;
  &lt;span class="na"&gt;AlertTopic&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::SNS::Topic&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;TopicName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drift-detection-alerts&lt;/span&gt;
      &lt;span class="na"&gt;Subscription&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Endpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;NotificationEmail&lt;/span&gt;
          &lt;span class="na"&gt;Protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;

&lt;span class="na"&gt;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;CentralEventBusArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;CentralEventBus.Arn&lt;/span&gt;
    &lt;span class="na"&gt;Export&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;DriftDetection-CentralBusArn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Spoke Accounts (Monitored Accounts)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# spoke-account-template.yaml&lt;/span&gt;
&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Drift&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Detection&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Spoke&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Monitored&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Account&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Setup'&lt;/span&gt;

&lt;span class="na"&gt;Parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;HubAccountId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hub account ID for central monitoring&lt;/span&gt;

  &lt;span class="na"&gt;MonitoringFrequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;String&lt;/span&gt;
    &lt;span class="na"&gt;Default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rate(6 hours)&lt;/span&gt;
    &lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;How often to check for drift&lt;/span&gt;

&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# AWS Config to detect changes&lt;/span&gt;
  &lt;span class="na"&gt;ConfigRecorder&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Config::ConfigurationRecorder&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;drift-detection-recorder&lt;/span&gt;
      &lt;span class="na"&gt;RoleArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;ConfigRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;RecordingGroup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AllSupported&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;ResourceTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::CloudFormation::Stack&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::EC2::Instance&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::RDS::DBInstance&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Function&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;

  &lt;span class="na"&gt;ConfigDeliveryChannel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Config::DeliveryChannel&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;drift-detection-channel&lt;/span&gt;
      &lt;span class="na"&gt;S3BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;ConfigBucket&lt;/span&gt;

  &lt;span class="na"&gt;ConfigBucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::S3::Bucket&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BucketName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;config-drift-${AWS::AccountId}-${AWS::Region}'&lt;/span&gt;
      &lt;span class="na"&gt;BucketEncryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ServerSideEncryptionConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ServerSideEncryptionByDefault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;SSEAlgorithm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AES256&lt;/span&gt;
      &lt;span class="na"&gt;VersioningConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
      &lt;span class="na"&gt;LifecycleConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&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;DeleteOldFiles&lt;/span&gt;
            &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
            &lt;span class="na"&gt;ExpirationInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;

  &lt;span class="na"&gt;ConfigRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::IAM::Role&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sts:AssumeRole&lt;/span&gt;
      &lt;span class="na"&gt;ManagedPolicyArns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::aws:policy/service-role/ConfigRole&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S3Access&lt;/span&gt;
          &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3:PutObject&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3:GetBucketAcl&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${ConfigBucket.Arn}/*'&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;ConfigBucket.Arn&lt;/span&gt;

  &lt;span class="c1"&gt;# Lambda to check drift periodically&lt;/span&gt;
  &lt;span class="na"&gt;DriftCheckerFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;drift-checker&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3.11&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;index.handler&lt;/span&gt;
      &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300&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;Variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;HUB_ACCOUNT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;HubAccountId&lt;/span&gt;
          &lt;span class="na"&gt;HUB_EVENT_BUS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:events:${AWS::Region}:${HubAccountId}:event-bus/DriftDetectionCentralBus'&lt;/span&gt;
      &lt;span class="na"&gt;Role&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftCheckerRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;Code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ZipFile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;import boto3&lt;/span&gt;
          &lt;span class="s"&gt;import json&lt;/span&gt;
          &lt;span class="s"&gt;import os&lt;/span&gt;
          &lt;span class="s"&gt;from datetime import datetime&lt;/span&gt;

          &lt;span class="s"&gt;cfn = boto3.client('cloudformation')&lt;/span&gt;
          &lt;span class="s"&gt;events = boto3.client('events')&lt;/span&gt;

          &lt;span class="s"&gt;def handler(event, context):&lt;/span&gt;
              &lt;span class="s"&gt;print("Starting drift detection scan...")&lt;/span&gt;

              &lt;span class="s"&gt;# Listar todas as stacks&lt;/span&gt;
              &lt;span class="s"&gt;paginator = cfn.get_paginator('list_stacks')&lt;/span&gt;
              &lt;span class="s"&gt;page_iterator = paginator.paginate(&lt;/span&gt;
                  &lt;span class="s"&gt;StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE']&lt;/span&gt;
              &lt;span class="s"&gt;)&lt;/span&gt;

              &lt;span class="s"&gt;detected_drifts = []&lt;/span&gt;

              &lt;span class="s"&gt;for page in page_iterator:&lt;/span&gt;
                  &lt;span class="s"&gt;for stack in page['StackSummaries']:&lt;/span&gt;
                      &lt;span class="s"&gt;stack_name = stack['StackName']&lt;/span&gt;
                      &lt;span class="s"&gt;print(f"Checking stack: {stack_name}")&lt;/span&gt;

                      &lt;span class="s"&gt;try:&lt;/span&gt;
                          &lt;span class="s"&gt;# Start drift detection&lt;/span&gt;
                          &lt;span class="s"&gt;detection_id = cfn.detect_stack_drift(&lt;/span&gt;
                              &lt;span class="s"&gt;StackName=stack_name&lt;/span&gt;
                          &lt;span class="s"&gt;)['StackDriftDetectionId']&lt;/span&gt;

                          &lt;span class="s"&gt;# Awaiting results&lt;/span&gt;
                          &lt;span class="s"&gt;waiter = cfn.get_waiter('stack_drift_detection_complete')&lt;/span&gt;
                          &lt;span class="s"&gt;waiter.wait(&lt;/span&gt;
                              &lt;span class="s"&gt;StackName=stack_name,&lt;/span&gt;
                              &lt;span class="s"&gt;StackDriftDetectionId=detection_id,&lt;/span&gt;
                              &lt;span class="s"&gt;WaiterConfig={'Delay': 5, 'MaxAttempts': 60}&lt;/span&gt;
                          &lt;span class="s"&gt;)&lt;/span&gt;

                          &lt;span class="s"&gt;# Get drift status&lt;/span&gt;
                          &lt;span class="s"&gt;drift_status = cfn.describe_stack_drift_detection_status(&lt;/span&gt;
                              &lt;span class="s"&gt;StackDriftDetectionId=detection_id&lt;/span&gt;
                          &lt;span class="s"&gt;)&lt;/span&gt;

                          &lt;span class="s"&gt;if drift_status['StackDriftStatus'] == 'DRIFTED':&lt;/span&gt;
                              &lt;span class="s"&gt;print(f"DRIFT DETECTED in {stack_name}")&lt;/span&gt;

                              &lt;span class="s"&gt;# Obtain resources with drift&lt;/span&gt;
                              &lt;span class="s"&gt;drifted_resources = []&lt;/span&gt;
                              &lt;span class="s"&gt;resource_drifts = cfn.describe_stack_resource_drifts(&lt;/span&gt;
                                  &lt;span class="s"&gt;StackName=stack_name,&lt;/span&gt;
                                  &lt;span class="s"&gt;StackResourceDriftStatusFilters=['MODIFIED', 'DELETED']&lt;/span&gt;
                              &lt;span class="s"&gt;)&lt;/span&gt;

                              &lt;span class="s"&gt;for resource in resource_drifts.get('StackResourceDrifts', []):&lt;/span&gt;
                                  &lt;span class="s"&gt;drifted_resources.append({&lt;/span&gt;
                                      &lt;span class="s"&gt;'LogicalId': resource['LogicalResourceId'],&lt;/span&gt;
                                      &lt;span class="s"&gt;'PhysicalId': resource.get('PhysicalResourceId'),&lt;/span&gt;
                                      &lt;span class="s"&gt;'ResourceType': resource['ResourceType'],&lt;/span&gt;
                                      &lt;span class="s"&gt;'DriftStatus': resource['StackResourceDriftStatus'],&lt;/span&gt;
                                      &lt;span class="s"&gt;'Differences': resource.get('PropertyDifferences', [])&lt;/span&gt;
                                  &lt;span class="s"&gt;})&lt;/span&gt;

                              &lt;span class="s"&gt;# Send event to hub account&lt;/span&gt;
                              &lt;span class="s"&gt;event_detail = {&lt;/span&gt;
                                  &lt;span class="s"&gt;'stack-id': stack['StackId'],&lt;/span&gt;
                                  &lt;span class="s"&gt;'stack-name': stack_name,&lt;/span&gt;
                                  &lt;span class="s"&gt;'drift-status': 'DRIFTED',&lt;/span&gt;
                                  &lt;span class="s"&gt;'detection-time': datetime.utcnow().isoformat(),&lt;/span&gt;
                                  &lt;span class="s"&gt;'drifted-resources': drifted_resources&lt;/span&gt;
                              &lt;span class="s"&gt;}&lt;/span&gt;

                              &lt;span class="s"&gt;events.put_events(&lt;/span&gt;
                                  &lt;span class="s"&gt;Entries=[{&lt;/span&gt;
                                      &lt;span class="s"&gt;'Source': 'custom.drift-detection',&lt;/span&gt;
                                      &lt;span class="s"&gt;'DetailType': 'Stack Drift Detected',&lt;/span&gt;
                                      &lt;span class="s"&gt;'Detail': json.dumps(event_detail),&lt;/span&gt;
                                      &lt;span class="s"&gt;'EventBusName': os.environ['HUB_EVENT_BUS']&lt;/span&gt;
                                  &lt;span class="s"&gt;}]&lt;/span&gt;
                              &lt;span class="s"&gt;)&lt;/span&gt;

                              &lt;span class="s"&gt;detected_drifts.append(stack_name)&lt;/span&gt;

                      &lt;span class="s"&gt;except Exception as e:&lt;/span&gt;
                          &lt;span class="s"&gt;print(f"Error checking {stack_name}: {str(e)}")&lt;/span&gt;
                          &lt;span class="s"&gt;continue&lt;/span&gt;

              &lt;span class="s"&gt;return {&lt;/span&gt;
                  &lt;span class="s"&gt;'statusCode': 200,&lt;/span&gt;
                  &lt;span class="s"&gt;'body': json.dumps({&lt;/span&gt;
                      &lt;span class="s"&gt;'message': 'Drift detection completed',&lt;/span&gt;
                      &lt;span class="s"&gt;'drifted_stacks': detected_drifts&lt;/span&gt;
                  &lt;span class="s"&gt;})&lt;/span&gt;
              &lt;span class="s"&gt;}&lt;/span&gt;

  &lt;span class="na"&gt;DriftCheckerRole&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::IAM::Role&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;AssumeRolePolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
        &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
            &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lambda.amazonaws.com&lt;/span&gt;
            &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sts:AssumeRole&lt;/span&gt;
      &lt;span class="na"&gt;ManagedPolicyArns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;PolicyName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DriftCheckerPolicy&lt;/span&gt;
          &lt;span class="na"&gt;PolicyDocument&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&lt;/span&gt;
            &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:DetectStackDrift&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:DescribeStackDriftDetectionStatus&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:DescribeStackResourceDrifts&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:ListStacks&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cloudformation:DescribeStacks&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
                &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;events:PutEvents&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:events:${AWS::Region}:${HubAccountId}:event-bus/DriftDetectionCentralBus'&lt;/span&gt;

  &lt;span class="c1"&gt;# EventBridge Rule for periodic trigger&lt;/span&gt;
  &lt;span class="na"&gt;DriftCheckSchedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Events::Rule&lt;/span&gt;
    &lt;span class="na"&gt;Properties&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;drift-check-schedule&lt;/span&gt;
      &lt;span class="na"&gt;ScheduleExpression&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;MonitoringFrequency&lt;/span&gt;
      &lt;span class="na"&gt;State&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ENABLED&lt;/span&gt;
      &lt;span class="na"&gt;Targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftCheckerFunction.Arn&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;drift-checker&lt;/span&gt;

  &lt;span class="na"&gt;SchedulePermission&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Lambda::Permission&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;DriftCheckerFunction&lt;/span&gt;
      &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lambda:InvokeFunction&lt;/span&gt;
      &lt;span class="na"&gt;Principal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;events.amazonaws.com&lt;/span&gt;
      &lt;span class="na"&gt;SourceArn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;DriftCheckSchedule.Arn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deployment via CLI
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Deploy in Hub Account&lt;/span&gt;
aws cloudformation create-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; drift-detection-hub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-body&lt;/span&gt; file://hub-account-template.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;NotificationEmail,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;sre-team@company.com &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;SpokeAccountIds,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"111111111111,222222222222"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="c"&gt;# Wait for creation to complete&lt;/span&gt;
aws cloudformation &lt;span class="nb"&gt;wait &lt;/span&gt;stack-create-complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; drift-detection-hub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1

&lt;span class="c"&gt;# 2. Deploy to each Spoke Account&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;ACCOUNT &lt;span class="k"&gt;in &lt;/span&gt;111111111111 222222222222&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deploying to account &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
  aws cloudformation create-stack &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; drift-detection-spoke &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--template-body&lt;/span&gt; file://spoke-account-template.yaml &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;HubAccountId,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;999999999999 &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MonitoringFrequency,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rate(4 hours)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--profile&lt;/span&gt; account-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ACCOUNT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Actual implementation results
&lt;/h4&gt;

&lt;h5&gt;
  
  
  First execution - Initial Discovery
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manual trigger for immediate testing&lt;/span&gt;
aws lambda invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; drift-checker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--invocation-type&lt;/span&gt; RequestResponse &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cli-binary-format&lt;/span&gt; raw-in-base64-out &lt;span class="se"&gt;\&lt;/span&gt;
  response.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; account-111111111111

&lt;span class="nb"&gt;cat &lt;/span&gt;response.json | jq &lt;span class="s1"&gt;'.'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Lambda's response:
&lt;/h6&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;"statusCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Drift detection completed"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"total_stacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"drifted_stacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"in_sync_stacks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"failed_checks"&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="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;"drifted_stacks"&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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-api-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database-cluster-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frontend-cdn-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monitoring-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-service-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data-pipeline-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backup-automation-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;h6&gt;
  
  
  Specific Drift Details
&lt;/h6&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stack-resource-drifts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; production-api-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-resource-drift-status-filters&lt;/span&gt; MODIFIED DELETED &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; account-111111111111
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Detailed response
&lt;/h6&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;"StackResourceDrifts"&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;"StackId"&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:cloudformation:us-east-1:111111111111:stack/production-api-stack/abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"LogicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"APIGatewayRestApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"PhysicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api-gw-prod-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;"ResourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::ApiGateway::RestApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedProperties"&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;"EndpointConfiguration"&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;"Types"&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="s2"&gt;"REGIONAL"&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;"MinimumCompressionSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&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;"ActualProperties"&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;"EndpointConfiguration"&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;"Types"&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="s2"&gt;"EDGE"&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;"MinimumCompressionSize"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2048&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;"PropertyDifferences"&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;"PropertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/EndpointConfiguration/Types/0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REGIONAL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ActualValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EDGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DifferenceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_EQUAL"&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;"PropertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/MinimumCompressionSize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1024"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ActualValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2048"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DifferenceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_EQUAL"&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;"StackResourceDriftStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MODIFIED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-15T14:32:45.123Z"&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;"LogicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LambdaFunction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"PhysicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-api-handler"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::Lambda::Function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"PropertyDifferences"&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;"PropertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/MemorySize"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"128"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ActualValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DifferenceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_EQUAL"&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;"PropertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/Timeout"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ActualValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"60"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DifferenceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_EQUAL"&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;"StackResourceDriftStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MODIFIED"&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;"LogicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DynamoDBTable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"PhysicalResourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api-data-table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::DynamoDB::Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"PropertyDifferences"&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;"PropertyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/ProvisionedThroughput/ReadCapacityUnits"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExpectedValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"ActualValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"DifferenceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NOT_EQUAL"&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;"StackResourceDriftStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MODIFIED"&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;h5&gt;
  
  
  Notification email received
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;From&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS Notifications &amp;lt;no-reply@sns.amazonaws.com&amp;gt;&lt;/span&gt;
&lt;span class="na"&gt;To&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;calbertocosta@cazalba.com.br&lt;/span&gt;
&lt;span class="na"&gt;Subject: 🚨 Drift Detected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production-api-stack&lt;/span&gt;
&lt;span class="na"&gt;Date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Wed, 8 Aug 2025 14:33:12&lt;/span&gt;

&lt;span class="s"&gt;⚠️ DRIFT DETECTED&lt;/span&gt;

&lt;span class="na"&gt;Account&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;111111111111&lt;/span&gt;  
&lt;span class="na"&gt;Region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;  
&lt;span class="na"&gt;Stack&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production-api-stack&lt;/span&gt;  

&lt;span class="s"&gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&lt;/span&gt;  
&lt;span class="s"&gt;Drifted Resources&lt;/span&gt;  
&lt;span class="s"&gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&lt;/span&gt;  

&lt;span class="s"&gt;🔸 **APIGatewayRestApi**&lt;/span&gt;  
&lt;span class="na"&gt;• PhysicalId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;api-gw-prod-123`&lt;/span&gt;  
&lt;span class="na"&gt;• ResourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;AWS::ApiGateway::RestApi`&lt;/span&gt;  
&lt;span class="na"&gt;• DriftStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;MODIFIED`&lt;/span&gt;  
&lt;span class="na"&gt;• Differences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;`/EndpointConfiguration/Types/0`&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;• Expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;REGIONAL`&lt;/span&gt;  
    &lt;span class="na"&gt;• Actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;EDGE`&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;`/MinimumCompressionSize`&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;• Expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;1024`&lt;/span&gt;  
    &lt;span class="na"&gt;• Actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;2048`&lt;/span&gt;  

&lt;span class="s"&gt;🔸 **LambdaFunction**&lt;/span&gt;  
&lt;span class="na"&gt;• ResourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;AWS::Lambda::Function`&lt;/span&gt;  
&lt;span class="na"&gt;• DriftStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;MODIFIED`&lt;/span&gt;  
&lt;span class="na"&gt;• Differences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;`/MemorySize`&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;• Expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;128`&lt;/span&gt;  
    &lt;span class="na"&gt;• Actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;512`&lt;/span&gt;  

&lt;span class="s"&gt;🔸 **DynamoDBTable**&lt;/span&gt;  
&lt;span class="na"&gt;• ResourceType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;AWS::DynamoDB::Table`&lt;/span&gt;  
&lt;span class="na"&gt;• DriftStatus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;MODIFIED`&lt;/span&gt;  
&lt;span class="na"&gt;• Differences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;`/ProvisionedThroughput/ReadCapacityUnits`&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  
    &lt;span class="na"&gt;• Expected&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;5`&lt;/span&gt;  
    &lt;span class="na"&gt;• Actual&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="s"&gt;25`&lt;/span&gt;  

&lt;span class="s"&gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&lt;/span&gt;  
&lt;span class="s"&gt;Action Required&lt;/span&gt;  
&lt;span class="s"&gt;━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━&lt;/span&gt;  

&lt;span class="s"&gt;Please review and update the CloudFormation template or revert the manual changes to maintain stack compliance.&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  DynamoDB Query - Complete history
&lt;/h6&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Complete scan of the table for analysis&lt;/span&gt;
aws dynamodb scan &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; drift-detection-history &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter-expression&lt;/span&gt; &lt;span class="s2"&gt;"driftStatus = :status"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expression-attribute-values&lt;/span&gt; &lt;span class="s1"&gt;'{":status":{"S":"DRIFTED"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--projection-expression&lt;/span&gt; &lt;span class="s2"&gt;"stackName,accountId,#t,driftedResources"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expression-attribute-names&lt;/span&gt; &lt;span class="s1"&gt;'{"#t":"timestamp"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json | jq &lt;span class="s1"&gt;'.Items[] | {stack: .stackName.S, account: .accountId.S, time: .timestamp.N, resources: .driftedResources.L | length}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Response
&lt;/h6&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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"production-api-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"111111111111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1736951520.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;"resources"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"database-cluster-prod"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"111111111111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1736951521.456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frontend-cdn-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"222222222222"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1736951522.789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;"stack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"monitoring-stack"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"111111111111"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1736951523.012"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="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;h5&gt;
  
  
  Website Metrics - Dashboard Visual
&lt;/h5&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%2Fs20vau03qz1ckbp2oa31.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%2Fs20vau03qz1ckbp2oa31.png" alt="First Page Dashboard Drift by Carlos Filho" width="800" height="408"&gt;&lt;/a&gt;&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%2F373j80cjouhbx1rco1w6.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%2F373j80cjouhbx1rco1w6.png" alt="Rest of the Page Dashboard Drift by Carlos Filho" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Query CloudWatch Metrics via CLI
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get drift metrics from the last 24 hours&lt;/span&gt;
aws cloudwatch get-metric-statistics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; DriftDetection &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; DriftDetected &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AccountId,Value&lt;span class="o"&gt;=&lt;/span&gt;111111111111 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; 2025-01-14T00:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; 2025-01-15T00:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 3600 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statistics&lt;/span&gt; Sum &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; json | jq &lt;span class="s1"&gt;'.Datapoints | sort_by(.Timestamp)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Metrics response
&lt;/h5&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T04:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T08:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T16:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;"Timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-14T20:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Sum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Count"&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;h5&gt;
  
  
  Trend analysis - Python
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Drift Detection Trend Analysis
Analyzes drift history and generates insights
&lt;/span&gt;&lt;span class="sh"&gt;"""&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;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="c1"&gt;# Initialize AWS clients
&lt;/span&gt;&lt;span class="n"&gt;dynamodb&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;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cloudwatch&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;cloudwatch&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decimal_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Helper for serializing DynamoDB Decimal&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_drift_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Analyzes drift history for the last N days&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;drift-detection-history&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate timestamp for N days ago
&lt;/span&gt;    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

    &lt;span class="c1"&gt;# Scan the table (in production, use Query with indexes)
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;FilterExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#ts &amp;gt; :start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeNames&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;#ts&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="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeValues&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;:start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Analysis by account
&lt;/span&gt;    &lt;span class="n"&gt;by_account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&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;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resources&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="c1"&gt;# Analysis by type of resource
&lt;/span&gt;    &lt;span class="n"&gt;by_resource_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Temporal analysis
&lt;/span&gt;    &lt;span class="n"&gt;by_day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;driftStatus&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DRIFTED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# By account
&lt;/span&gt;            &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accountId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;by_account&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

            &lt;span class="c1"&gt;# By type of resource
&lt;/span&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;item&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;driftedResources&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;resource_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&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;ResourceType&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="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;by_resource_type&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="n"&gt;by_account&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;resources&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Per day
&lt;/span&gt;            &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;by_day&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate statistics
&lt;/span&gt;    &lt;span class="n"&gt;total_drifts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;driftStatus&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DRIFTED&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;total_resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_resource_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Top 5 features with the most drift
&lt;/span&gt;    &lt;span class="n"&gt;top_resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_resource_type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Most problematic account
&lt;/span&gt;    &lt;span class="n"&gt;most_problematic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;by_account&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;report&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;analysis_period&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days&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="s"&gt;total_drift_detections&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_drifts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_resources_affected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;average_drift_per_day&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_drifts&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;by_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_account&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_drifted_resource_types&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;top_resources&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;most_problematic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily_trend&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_day&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_realtime_metrics&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Get real-time metrics from CloudWatch&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;start_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloudwatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_metric_statistics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DriftDetection&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;MetricName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DriftedResourceCount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;StartTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;start_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;EndTime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;end_time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 1 hora
&lt;/span&gt;        &lt;span class="n"&gt;Statistics&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;Sum&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="s"&gt;Average&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="s"&gt;Maximum&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;datapoints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Datapoints&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;metrics&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;last_24h&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_resources_drifted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Sum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;datapoints&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;peak_hour_drift&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Maximum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;datapoints&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;average_drift_per_hour&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Average&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;datapoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datapoints&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;datapoints&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hourly_data&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="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hour&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%H:%M&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="s"&gt;drift_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Sum&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;datapoints&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_recommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generates recommendations based on analysis&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;recommendations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="c1"&gt;# Recommendation based on problematic account
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;drift_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&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="s"&gt;HIGH&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="s"&gt;type&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="s"&gt;ACCOUNT_REVIEW&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="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Account &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;drift_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; drift detections. Review IAM permissions and enforce Infrastructure as Code practices.&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="s"&gt;action&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="s"&gt;Review CloudTrail logs for manual changes and implement preventive controls&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Recommendation based on resource types
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_drifted_resource_types&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;top_resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_drifted_resource_types&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&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="s"&gt;MEDIUM&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="s"&gt;type&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="s"&gt;RESOURCE_PATTERN&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="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;top_resource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; resources show the most drift (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;top_resource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; occurrences)&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="s"&gt;action&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="s"&gt;Consider implementing AWS Config Rules for automatic remediation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Recommendation based on trend
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;average_drift_per_day&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&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="s"&gt;HIGH&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="s"&gt;type&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="s"&gt;TREND_ALERT&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="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;High drift rate detected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;average_drift_per_day&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; drifts per day&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="s"&gt;action&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="s"&gt;Implement stricter change management process and automated drift remediation&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;recommendations&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Main execution&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DRIFT DETECTION ANALYSIS REPORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generated at: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Historical analysis
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HISTORICAL ANALYSIS (Last 7 Days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;analysis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;analyze_drift_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Drift Detections: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_drift_detections&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Resources Affected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total_resources_affected&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average Drift per Day: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;average_drift_per_day&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Top 5 Drifted Resource Types:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;top_drifted_resource_types&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resource_type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; occurrences&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Most Problematic Account:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Account ID: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Drift Count: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;most_problematic_account&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Real-time metrics
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;REAL-TIME METRICS (Last 24 Hours)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_realtime_metrics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Total Resources Drifted: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;last_24h&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="s"&gt;total_resources_drifted&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Peak Hour Drift: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;last_24h&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="s"&gt;peak_hour_drift&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average Drift per Hour: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;last_24h&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="s"&gt;average_drift_per_hour&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Daily trend
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;DAILY TREND:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;daily_trend&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;█&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Recommendations
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;RECOMMENDATIONS:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;recommendations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_recommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recommendations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;emoji&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🔴&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🟡&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;emoji&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;priority&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Issue: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;   Action: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Export to JSON
&lt;/span&gt;    &lt;span class="n"&gt;output&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;timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;analysis&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;analysis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;metrics&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;recommendations&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;recommendations&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;drift_report.json&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="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decimal_default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;✅ Full report saved to drift_report.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Running the Analysis Script
&lt;/h5&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Execute analysis&lt;/span&gt;
python3 drift_analysis.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h5&gt;
  
  
  Output script:
&lt;/h5&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%2Fppk1kg9cpzjr7gi5c8hn.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%2Fppk1kg9cpzjr7gi5c8hn.png" alt="Output by Carlos Filho's terminal" width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h5&gt;
  
  
  Contents of the generated JSON report
&lt;/h5&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;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-11T14:45:23.456789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"analysis"&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;"analysis_period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7 days"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_drift_detections"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_resources_affected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;183&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"average_drift_per_day"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.71&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"by_account"&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;"111111111111"&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;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="s2"&gt;"AWS::Lambda::Function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::ApiGateway::RestApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::DynamoDB::Table"&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;"222222222222"&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;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="s2"&gt;"AWS::S3::Bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::RDS::DBInstance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::Lambda::Function"&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;"333333333333"&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;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="s2"&gt;"AWS::CloudWatch::Alarm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::EC2::Instance"&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;"top_drifted_resource_types"&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="s2"&gt;"AWS::Lambda::Function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;45&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="s2"&gt;"AWS::ApiGateway::RestApi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&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="s2"&gt;"AWS::DynamoDB::Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&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="s2"&gt;"AWS::RDS::DBInstance"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&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="s2"&gt;"AWS::S3::Bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&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;"daily_trend"&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;"2025-08-10"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"2025-08-09"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"2025-08-08"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"2025-08-07"&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;"2025-08-06"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"2025-08-05"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"2025-08-04"&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="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;"metrics"&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;"last_24h"&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;"total_resources_drifted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"peak_hour_drift"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"average_drift_per_hour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.75&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;"recommendations"&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;"priority"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ACCOUNT_REVIEW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Account 111111111111 has 23 drift detections"&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;"Review CloudTrail logs and implement preventive controls"&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;h3&gt;
  
  
  Achieved results - Real metrics
&lt;/h3&gt;

&lt;p&gt;If we can imagine this type of situation frequently appears in real environments where multiple teams make changes. With this solution implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;78% reduction in detection time&lt;/strong&gt; &amp;gt; from 5 days to 4 hours*&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can save thousand of dollars per month&lt;/strong&gt; &amp;gt; avoiding unintentional over-provisioning*&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance increased to 91%&lt;/strong&gt; &amp;gt; previously at 67%*&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MTTR reduced by 65%&lt;/strong&gt; &amp;gt; more reliable rollbacks*&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero production incidents due to add, adjust or remove&lt;/strong&gt; caused by undetected drift in the last 30 days*&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;*I tested in my account where I use constantly and have several workloads running.&lt;br&gt;
A SCP helps a lot to avoid unexpected changes. AWS Config is a big friend here, so if you change the collect data from Continous to Daily the information can take a time to reflect and receive a notification about the drift.&lt;/p&gt;

&lt;p&gt;It's common to observe that teams take days to notice drift in production. When these areas work together, the impact tends to be clearer, and we managed to transform an invisible problem into actionable data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;p&gt;To be honest I am impressed what I've created, this means I achieved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total visibility&lt;/strong&gt; &amp;gt; No change goes unnoticed
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immediate response&lt;/strong&gt; &amp;gt; Real-time alerts reach the SRE team as soon as something shifts
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete history&lt;/strong&gt; &amp;gt; All changes are logged for trend analysis and compliance tracking
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimized cost&lt;/strong&gt; &amp;gt; Built entirely with AWS managed services, with no unnecessary overhead
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; &amp;gt; Designed to scale effortlessly across hundreds of AWS accounts
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thank you, see you next time.&lt;/p&gt;

</description>
      <category>eventbridge</category>
      <category>aws</category>
      <category>cloudformation</category>
      <category>configaws</category>
    </item>
    <item>
      <title>Solving cold start in AWS Lambda with intelligent distributed Cache</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Wed, 27 Aug 2025 17:09:38 +0000</pubDate>
      <link>https://dev.to/aws-builders/solving-cold-start-in-aws-lambda-with-intelligent-distributed-cache-3g6n</link>
      <guid>https://dev.to/aws-builders/solving-cold-start-in-aws-lambda-with-intelligent-distributed-cache-3g6n</guid>
      <description>&lt;p&gt;You know that frustration when your serverless API takes 3 seconds on the first request?&lt;/p&gt;

&lt;p&gt;In practice, this means you migrated to Lambda expecting cost savings, but your users are complaining about random timeouts. It's common to observe that the problem goes far beyond the cold container your function needs to reconnect to RDS, fetch secrets, reload configurations...&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I built&lt;/strong&gt;: An intelligent warming system that analyzes usage patterns and keeps your functions always ready.&lt;/p&gt;

&lt;h1&gt;
  
  
  Smart Lambda Warmer Architecture
&lt;/h1&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%2Fv67gp5rndlknoat6yrto.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%2Fv67gp5rndlknoat6yrto.png" alt=" " width="800" height="278"&gt;&lt;/a&gt;&lt;br&gt;
(&lt;em&gt;Smart Lambda Warmer Architecture&lt;/em&gt;)&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works:
&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;# Complete deployment via CLI - no console needed!&lt;/span&gt;
git clone https://github.com/cazalba/smart-lambda-warmer
&lt;span class="nb"&gt;cd &lt;/span&gt;smart-lambda-warmer

&lt;span class="c"&gt;# Automatic deployment with Python&lt;/span&gt;
python deploy.py &lt;span class="nt"&gt;--stack-name&lt;/span&gt; production-warmer &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Actual deployment response:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Smart Lambda Warmer Deployment
================================
Discovering VPC configuration...
- Found VPC: vpc-0a1b2c3d4e5f6g7h8
- Found Subnets: subnet-02453436629abcdef0, subnet-fedcba9876543210
- Packaging Pattern Analyzer function...
- Packaging Smart Warmer function...
- Lambda packages uploaded to S3
- Deploying stack: production-warmer
- Stack deployed successfully!
- Dashboard URL: https://console.aws.amazon.com/cloudwatch/home
- Redis Endpoint: smart-warmer-redis.cazalba-post-lambda.cache.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Intelligent Analysis Flow
&lt;/h3&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%2Fi0p2gnag3z8gb93ipx7g.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%2Fi0p2gnag3z8gb93ipx7g.png" alt=" " width="800" height="139"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-time pattern analysis:
&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 pattern analysis&lt;/span&gt;
aws lambda invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; production-warmer-pattern-analyzer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-type&lt;/span&gt; Tail response.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Pattern analyzer response:
&lt;/h4&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;"analyzed_functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"classifications"&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;"user-api-get-profile"&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;"avg_invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;45.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"strategy"&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_TRAFFIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"warming_interval"&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;"concurrent"&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="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;"payment-processor"&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;"avg_invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;8.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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MEDIUM_TRAFFIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"warming_interval"&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;"concurrent"&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;"report-generator"&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;"avg_invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.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;"strategy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"LOW_TRAFFIC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"warming_interval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"concurrent"&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="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;h3&gt;
  
  
  Real Performance results
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Load Test - BEFORE vs AFTER:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Artillery test&lt;/span&gt;
artillery quick &lt;span class="nt"&gt;--count&lt;/span&gt; 100 &lt;span class="nt"&gt;--num&lt;/span&gt; 10 https://api.example.com/user/profile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Metric&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;BEFORE&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;AFTER&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Improvement&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean Response&lt;/td&gt;
&lt;td&gt;1847.3ms&lt;/td&gt;
&lt;td&gt;143.7ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92.2% ✅&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 Latency&lt;/td&gt;
&lt;td&gt;3127ms&lt;/td&gt;
&lt;td&gt;189ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;94.0% ✅&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P99 Latency&lt;/td&gt;
&lt;td&gt;3698ms&lt;/td&gt;
&lt;td&gt;234ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93.7% ✅&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeouts&lt;/td&gt;
&lt;td&gt;23 errors&lt;/td&gt;
&lt;td&gt;0 errors&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% ✅&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold Starts&lt;/td&gt;
&lt;td&gt;47/100&lt;/td&gt;
&lt;td&gt;3/100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;93.6% ✅&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Metrics evolution (Timeline):
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hour&lt;/th&gt;
&lt;th&gt;Avg (ms)&lt;/th&gt;
&lt;th&gt;Max (ms)&lt;/th&gt;
&lt;th&gt;Min (ms)&lt;/th&gt;
&lt;th&gt;Errors&lt;/th&gt;
&lt;th&gt;Cold Starts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hour 0 (Baseline)&lt;/td&gt;
&lt;td&gt;1432&lt;/td&gt;
&lt;td&gt;3127&lt;/td&gt;
&lt;td&gt;98&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;47%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour 1 (Learning)&lt;/td&gt;
&lt;td&gt;387&lt;/td&gt;
&lt;td&gt;812&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hour 2 (Optimized)&lt;/td&gt;
&lt;td&gt;124&lt;/td&gt;
&lt;td&gt;201&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Attribute - Applied Machine Learning
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Predictive analysis with numpy - actual solution code
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;analyze_pattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Adaptive algorithm based on real patterns
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;invocations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Sum&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Datapoints&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;std_deviation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;peak_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Intelligent classification based on standard deviation
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;std_deviation&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Consistent high demand
&lt;/span&gt;        &lt;span class="n"&gt;strategy&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;warming_interval&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;concurrent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classification&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="s"&gt;HIGH_STABLE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;peak_factor&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# High demand with bursts
&lt;/span&gt;        &lt;span class="n"&gt;strategy&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;warming_interval&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;concurrent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invocations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classification&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="s"&gt;HIGH_BURST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;avg_invocations&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Medium demand
&lt;/span&gt;        &lt;span class="n"&gt;strategy&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;warming_interval&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;concurrent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classification&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="s"&gt;MEDIUM&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Low demand
&lt;/span&gt;        &lt;span class="n"&gt;strategy&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;warming_interval&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;concurrent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;classification&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="s"&gt;LOW&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Actual Smart Warmer execution with concurrency:
&lt;/h4&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;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-07T10:47:32Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warmed_functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"total_warming_invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"results"&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;"user-api-get-profile"&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;"concurrent_executions"&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;"results"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cold"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cold"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cold"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;287&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cold"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"D"&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="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cold"&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;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"container"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"E"&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;"success_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"containers_warmed"&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="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;"cache_performance"&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;"hits"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"misses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hit_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100%"&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;
  
  
  CloudWatch dashboard - Real-time metrics
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create custom metrics&lt;/span&gt;
aws cloudwatch put-metric-data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; SmartWarmer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; ColdStartsPrevented &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--value&lt;/span&gt; 7231 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;AllFunctions,Period&lt;span class="o"&gt;=&lt;/span&gt;Week
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Dashboard Metrics (Live):
&lt;/h4&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%2F1u2gz3axism1p5itfg7n.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%2F1u2gz3axism1p5itfg7n.png" alt="Dashboard Metrics (Live)" width="624" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Weekly Report via SNS
&lt;/h3&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;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"🎯 Smart Warmer Weekly Report - 87% Improvement"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-07T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-01 to 2025-08-07"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"executive_summary"&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;"cold_starts_prevented"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7231&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"average_latency_improvement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"87.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;"timeout_errors_prevented"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;412&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"user_experience_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A+ (was C-)"&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;"cost_benefit_analysis"&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;"warming_invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60480&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"warming_cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$11.87"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeout_penalties_avoided"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$156.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"additional_revenue_from_ux"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$892.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_benefit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$1048.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"roi_percentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8726%"&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;"top_optimized_functions"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-api-get-profile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7824&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cold_starts_prevented"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2834&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"avg_latency_before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1523ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"avg_latency_after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"128ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"improvement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"91.6%"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment-processor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"invocations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cold_starts_prevented"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;743&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"avg_latency_before"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2100ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"avg_latency_after"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"245ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"improvement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"88.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="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ml_insights"&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;"peak_hours_detected"&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="s2"&gt;"09:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"14:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"20:00"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"weekend_pattern"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"60% less traffic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prediction_accuracy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"94.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;"recommendations"&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;" Increase concurrent warming for 'order-service' (traffic grew 23%)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;" Consider removing 'legacy-reporter' from warming (0.1 req/hour)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;" Enable predictive warming for Black Friday preparation"&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;h3&gt;
  
  
  Complete production deployment
&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;# 1. Validate CloudFormation template&lt;/span&gt;
aws cloudformation validate-template &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-body&lt;/span&gt; file://template.yaml

&lt;span class="c"&gt;# 2. Deploy with all configurations&lt;/span&gt;
aws cloudformation create-stack &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; smart-warmer-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-body&lt;/span&gt; file://template.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameters&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Environment,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;production &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ParameterKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;VpcId,ParameterValue&lt;span class="o"&gt;=&lt;/span&gt;vpc-xxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_IAM &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tags&lt;/span&gt; &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Project,Value&lt;span class="o"&gt;=&lt;/span&gt;ServerlessOptimization &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;CostCenter,Value&lt;span class="o"&gt;=&lt;/span&gt;Engineering &lt;span class="se"&gt;\&lt;/span&gt;
         &lt;span class="nv"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Owner,Value&lt;span class="o"&gt;=&lt;/span&gt;DevOps

&lt;span class="c"&gt;# 3. Monitor progress&lt;/span&gt;
aws cloudformation &lt;span class="nb"&gt;wait &lt;/span&gt;stack-create-complete &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; smart-warmer-prod

&lt;span class="c"&gt;# 4. Check outputs&lt;/span&gt;
aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; smart-warmer-prod &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Final Stack output:
&lt;/h4&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;"StackStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATE_COMPLETE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"CreationTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-08-07T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Outputs"&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;"OutputKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RedisEndpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smart-warmer-redis.cazalba-post-lambda.cache.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Redis cluster for strategy caching"&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;"OutputKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DashboardURL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://console.aws.amazon.com/cloudwatch/dashboards/smart-warmer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Real-time performance monitoring"&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;"OutputKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PatternAnalyzerArn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputValue"&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:lambda:us-east-1:4258222134:function:pattern-analyzer"&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;"OutputKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SmartWarmerArn"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputValue"&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:lambda:us-east-1:8562123522:function:smart-warmer"&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;"EnableTerminationProtection"&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="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;
  
  
  Comparison with other solutions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;th&gt;Our Approach&lt;/th&gt;
&lt;th&gt;Provisioned Concurrency&lt;/th&gt;
&lt;th&gt;Lambda Extensions&lt;/th&gt;
&lt;th&gt;Manual Warming&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly cost&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$12&lt;/td&gt;
&lt;td&gt;$180+&lt;/td&gt;
&lt;td&gt;$45&lt;/td&gt;
&lt;td&gt;$8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Effectiveness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Complexity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Adaptability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ML/Analytics&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ROI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8726%&lt;/td&gt;
&lt;td&gt;-200%&lt;/td&gt;
&lt;td&gt;156%&lt;/td&gt;
&lt;td&gt;250%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Production lessons learned
&lt;/h2&gt;

&lt;p&gt;What's not always evident is that the secret isn't just warming functions, but deeply understanding their usage patterns. This type of situation frequently appears in real environments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Distributed cache is the point: Coordination between warmers avoids duplication&lt;/li&gt;
&lt;li&gt;Analysis should be hourly, not real-time: Reduces costs and improves accuracy&lt;/li&gt;
&lt;li&gt;Concurrent executions: Difference between 200ms and 2000ms in critical APIs&lt;/li&gt;
&lt;li&gt;Seasonal patterns: Friday has 40% less traffic than Monday&lt;/li&gt;
&lt;li&gt;Cost vs Benefit: $12/month prevents $1000+ in losses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When these areas work together, the impact tends to be clearer - you not only solve cold starts but gain reasonable insights about your system.&lt;/p&gt;

&lt;p&gt;Thank you, see you next time!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>machinelearning</category>
      <category>lambda</category>
    </item>
    <item>
      <title>I created a solution for AWS called Anomaly Guardian</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Fri, 25 Jul 2025 20:34:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/i-created-a-solution-for-aws-called-anomaly-guardian-3h9j</link>
      <guid>https://dev.to/aws-builders/i-created-a-solution-for-aws-called-anomaly-guardian-3h9j</guid>
      <description>&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%2Fliiwa9lbyiy1ezw0u2kq.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%2Fliiwa9lbyiy1ezw0u2kq.png" alt=" " width="536" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Anomaly Guardian – A Real-Time Anomaly Detection and Response System
&lt;/h1&gt;

&lt;h2&gt;
  
  
  What is this solution?
&lt;/h2&gt;

&lt;p&gt;In practice, this means that the system was designed to identify anomalies in real time and respond to them automatically, without relying on human intervention. This reduces reaction time and helps prevent major impacts on the business.&lt;/p&gt;

&lt;p&gt;What is not always evident is that this autonomy is only possible because the solution is based on a robust architecture that combines different AWS services at a more advanced technical level. This integration allows &lt;strong&gt;Anomaly Guardian&lt;/strong&gt; to function as a kind of digital watchdog, always alert, analyzing data as it is generated and making decisions based on non-standard behavior.&lt;/p&gt;

&lt;p&gt;This type of approach often appears in companies that have experienced problems with unexpected cost spikes or operational failures at critical times. When these areas — monitoring, automation, and response — work together, the impact tends to be clearer: fewer surprises, more control, and an engineering team with more time to focus on what matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes this solution different in practice
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simultaneous multi-pronged analysis&lt;/strong&gt; – User behavior, transactions, location, and other signals are evaluated together to identify relevant deviations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic response to known incidents&lt;/strong&gt; – If something has already been mapped as a risk, the system resolves it before anyone even notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships that make sense&lt;/strong&gt; – When an anomaly arises, the correlation engine cross-references events to understand if there is a common cause or a larger failure behind it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ability to predict and act before the problem arises&lt;/strong&gt; – Based on what has happened before, the system scales or adjusts resources to avoid bottlenecks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Designed for multi-client environments&lt;/strong&gt; – Each company operates independently, with total security and privacy within the same structure.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Solution Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Data Ingestion Layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[API Gateway (REST API)] --&amp;gt; [Kinesis Data Streams] --&amp;gt; [Lambda Event Processor]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Services used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon API Gateway&lt;/strong&gt;
Acts as the main RESTful entry point, collecting events from distributed systems or user interactions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Kinesis Data Streams&lt;/strong&gt;
Handles real-time event streaming, ensuring events are reliably captured and queued.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt;
Performs the first layer of logic filtering, enriching, or routing events as needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Technical characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingestion rate above &lt;strong&gt;10,000 events/second&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Latency below &lt;strong&gt;100ms&lt;/strong&gt; for initial processing&lt;/li&gt;
&lt;li&gt;Auto-scaling behavior according to workload volume&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. ML and Analytics Layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Kinesis Analytics] --&amp;gt; [SageMaker Endpoint] --&amp;gt; [Elasticsearch (Indexing)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Services used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Kinesis Analytics&lt;/strong&gt;
Performs SQL-based real-time analysis to identify behavioral patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SageMaker&lt;/strong&gt;
Hosts machine learning models that classify anomalies on the fly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Lookout for Metrics&lt;/strong&gt;
Monitors key business metrics and detects anomalies automatically — no manual rules required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Elasticsearch (OpenSearch)&lt;/strong&gt;
Indexes processed events to enable complex querying and pattern detection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Algorithms implemented:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolation Forest&lt;/strong&gt; – Detects outliers in high-dimensional data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statistical Process Control (SPC)&lt;/strong&gt; – Used to monitor and manage process variations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Learning Autoencoders&lt;/strong&gt; – Ideal for identifying temporal anomalies in sequences.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correlation Analysis&lt;/strong&gt; – Finds relationships between multiple variables to uncover contextual anomalies.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Intelligence and Response Layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Step Functions (Orchestration)] --&amp;gt; [Security Actions (Auto-remediation)] --&amp;gt; [Incident Response]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Services used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Step Functions&lt;/strong&gt;
Coordinates complex workflows based on rule evaluation and event triggers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon SNS&lt;/strong&gt;
Publishes alerts across multiple channels (email, SMS, etc.), keeping teams informed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt;
Executes targeted response actions based on incident type or rule match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon DynamoDB&lt;/strong&gt;
Stores event history, decision trees, and remediation rules in a scalable way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Types of Detected Anomalies
&lt;/h2&gt;

&lt;p&gt;The system is designed to recognize different kinds of deviations in user behavior, transactions, and infrastructure. Each type of anomaly triggers a specific detection method and response path.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Velocity Anomalies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; Unusual number of requests from the same user or IP address
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold:&lt;/strong&gt; More than &lt;strong&gt;100 requests&lt;/strong&gt; within &lt;strong&gt;5 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; Automatic &lt;strong&gt;rate limiting&lt;/strong&gt; to mitigate abuse&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2. Value Anomalies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; Transactions involving unusually large or inconsistent amounts
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; Detection based on &lt;strong&gt;moving standard deviation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; &lt;strong&gt;Manual review&lt;/strong&gt; triggered for any transaction above &lt;strong&gt;$5,000&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3. Geolocation Anomalies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; Sudden changes in user’s geographic location
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; &lt;strong&gt;Time-series analysis&lt;/strong&gt; to track location shifts
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; &lt;strong&gt;Extra layer of security validation&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4. Behavioral Anomalies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; Unexpected or erratic browsing patterns
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; &lt;strong&gt;Session-based Machine Learning model&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; &lt;strong&gt;Intensive monitoring mode&lt;/strong&gt; enabled&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5. System Anomalies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; Drops in performance or availability
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Algorithm:&lt;/strong&gt; &lt;strong&gt;Statistical Process Control (SPC)&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response:&lt;/strong&gt; &lt;strong&gt;Auto-scaling&lt;/strong&gt; and &lt;strong&gt;workload rebalancing&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Configuration and Deployment
&lt;/h2&gt;

&lt;p&gt;Setting up the &lt;strong&gt;Anomaly Guardian&lt;/strong&gt; is straightforward, as long as the basic environment and permissions are in place. Below is a quick guide to ensure everything runs smoothly from the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&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;# AWS CLI v2+&lt;/span&gt;
aws &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# Python 3.9+&lt;/span&gt;
python3 &lt;span class="nt"&gt;--version&lt;/span&gt;

&lt;span class="c"&gt;# Required IAM Permissions:&lt;/span&gt;
&lt;span class="c"&gt;# (All services need Full Access for deployment purposes)&lt;/span&gt;
- CloudFormation
- SageMaker
- Kinesis
- Lambda
- DynamoDB
- Elasticsearch
- Step Functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automated Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Clone the templates&lt;/span&gt;
git clone &amp;lt;repository&amp;gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;anomaly-guardian

&lt;span class="c"&gt;# Set up environment variables&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"anomaly-guardian"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"prod"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;EMAIL_ALERT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"admin@company.com"&lt;/span&gt;

&lt;span class="c"&gt;# Run the deployment&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x deploy.sh
./deploy.sh deploy&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Manual Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Base Infrastructure Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; anomaly-guardian-base.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; anomaly-guardian-prod-base &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ProjectName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anomaly-guardian &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;AlertEmail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin@company.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_NAMED_IAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Advanced Components Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--template-file&lt;/span&gt; anomaly-guardian-advanced.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stack-name&lt;/span&gt; anomaly-guardian-prod-advanced &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parameter-overrides&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;BaseStackName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anomaly-guardian-prod-base &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nv"&gt;ProjectName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;anomaly-guardian &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_NAMED_IAM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring and Observability
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Business Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anomalies Detected/Hour:
Measures how many anomalies are identified every hour. Useful for spotting spikes or patterns in unusual behavior.&lt;/li&gt;
&lt;li&gt;False Positive Rate:
Helps evaluate model precision. A high rate might indicate overly aggressive detection rules.&lt;/li&gt;
&lt;li&gt;Mean Time to Detection (MTTD):
Tracks how long it typically takes to detect an anomaly after it occurs.&lt;/li&gt;
&lt;li&gt;Mean Time to Response (MTTR):
Measures the average time between detection and mitigation or escalation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technical Metrics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway Latency:
Observes the responsiveness of the ingestion endpoint.&lt;/li&gt;
&lt;li&gt;Lambda Duration:
Indicates how long your functions are taking to execute. Spikes may hint at processing bottlenecks.&lt;/li&gt;
&lt;li&gt;Kinesis Incoming/Outgoing Records:
Tracks throughput for streaming data is important for ensuring no event backlog.&lt;/li&gt;
&lt;li&gt;SageMaker Endpoint Utilization:
Reflects how actively the ML model is being used for inference.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CloudWatch Dashboards
&lt;/h2&gt;

&lt;p&gt;To make monitoring more accessible, the system automatically generates a custom CloudWatch dashboard with widgets that group key metrics by function.&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;"widgets"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anomaly Detection Overview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&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="s2"&gt;"AnomalyDetected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AnomalyScore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ResponseTime"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System Health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&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="s2"&gt;"Lambda Errors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kinesis Throughput"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SageMaker Utilization"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security Actions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metrics"&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="s2"&gt;"Users Blocked"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rate Limited"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Incidents Created"&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;h2&gt;
  
  
  Configured alerts
&lt;/h2&gt;

&lt;p&gt;The system continuously monitors critical thresholds to trigger automated alerts. These alerts are divided into two levels: Critical and Warning, based on severity and impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  Critical alerts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anomaly Score &amp;gt; 0.9 for 3 consecutive periods
Indicates highly suspicious behavior sustained over time.&lt;/li&gt;
&lt;li&gt;Lambda Error Rate &amp;gt; 5%
A spike in function errors, usually signaling a misconfiguration or downstream issue.&lt;/li&gt;
&lt;li&gt;SageMaker Latency &amp;gt; 5 seconds
When model response time degrades significantly, this alert is triggered to prevent detection delays.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Warning alerts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anomaly Score &amp;gt; 0.7 for 5 periods:
Less severe but still notable often an early sign of unusual behavior emerging.&lt;/li&gt;
&lt;li&gt;Kinesis Utilization &amp;gt; 80%:
May indicate stream saturation; important to track before hitting capacity limits.&lt;/li&gt;
&lt;li&gt;Event Correlation Failure:
Triggered when expected correlations across dimensions break, which might signal fragmented or inconsistent data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security
&lt;/h2&gt;

&lt;p&gt;Security is embedded at every layer of the architecture from encrypted communication channels to granular access policies and network isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cryptography
&lt;/h3&gt;

&lt;p&gt;In transit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TLS 1.2+ enforced across all communications&lt;/li&gt;
&lt;li&gt;SSL/TLS certificates managed through AWS Certificate Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kinesis: Encrypted with AWS-managed KMS keys&lt;/li&gt;
&lt;li&gt;DynamoDB: Server-side encryption enabled&lt;/li&gt;
&lt;li&gt;S3: AES-256 encryption for stored objects&lt;/li&gt;
&lt;li&gt;Elasticsearch: Encryption at rest enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Access control
&lt;/h3&gt;

&lt;p&gt;IAM Roles and Policies:&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;"kinesis:PutRecord"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"kinesis:PutRecords"&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="s2"&gt;"arn:aws:kinesis:*:*:stream/anomaly-guardian-*"&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;h3&gt;
  
  
  VPC and Network Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Critical resources are deployed in private subnets&lt;/li&gt;
&lt;li&gt;Security Groups follow least privilege access rules&lt;/li&gt;
&lt;li&gt;Network ACLs (NACLs) are used for additional subnet-level control&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Audit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CloudTrail integration
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Logs all administrative actions performed within the environment.&lt;/li&gt;
&lt;li&gt;Integrated with AWS Config for configuration tracking and compliance auditing.&lt;/li&gt;
&lt;li&gt;Log retention configured for 7 years, supporting long-term audit requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GDPR
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Pseudonymization of personal data where applicable.&lt;/li&gt;
&lt;li&gt;Right to be forgotten implementation included for sensitive datasets.&lt;/li&gt;
&lt;li&gt;Clearly defined data retention policies in line with data protection laws.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance and Scalability
&lt;/h2&gt;

&lt;p&gt;Understanding how the system performs under load and how it scales is key to ensuring it can handle real-world demands without degradation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance Benchmarks
&lt;/h3&gt;

&lt;p&gt;Maximum Throughput&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway: Up to 10,000 RPS (with caching enabled)&lt;/li&gt;
&lt;li&gt;Kinesis: Scales with 1,000 shards (1MB/s per shard)&lt;/li&gt;
&lt;li&gt;Lambda: Supports 1,000 concurrent executions&lt;/li&gt;
&lt;li&gt;SageMaker: Up to 1,000 inferences per second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Typical Latency&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;P50 (median): &amp;lt; 50ms (ingestion + initial processing)&lt;/li&gt;
&lt;li&gt;P95: &amp;lt; 200ms&lt;/li&gt;
&lt;li&gt;P99: &amp;lt; 500ms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Auto-Scaling Configuration
&lt;/h3&gt;

&lt;p&gt;Horizontal Scaling&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="na"&gt;SageMaker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;MinInstances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;MaxInstances&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;TargetUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;70%&lt;/span&gt;

&lt;span class="na"&gt;Lambda&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ReservedConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;BurstConcurrency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;

&lt;span class="na"&gt;Kinesis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AutoScaling&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
  &lt;span class="na"&gt;TargetUtilization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;70%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vertical Scaling&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SageMaker endpoints scale automatically based on inference load.&lt;/li&gt;
&lt;li&gt;DynamoDB uses on-demand billing, scaling read/write capacity as needed.&lt;/li&gt;
&lt;li&gt;Elasticsearch (OpenSearch) includes auto-scaling for storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Maintenance and Operations
&lt;/h2&gt;

&lt;p&gt;Operational excellence isn’t just about automation it also involves structured routines and a clear playbook for common issues.&lt;/p&gt;

&lt;h4&gt;
  
  
  Daily
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Health metrics verification&lt;/li&gt;
&lt;li&gt;Review of critical anomalies&lt;/li&gt;
&lt;li&gt;Configuration backup procedures&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Weekly
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;ML model updates&lt;/li&gt;
&lt;li&gt;Review of false positives&lt;/li&gt;
&lt;li&gt;Threshold optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Monthly
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Trend analysis and anomaly patterns&lt;/li&gt;
&lt;li&gt;Capacity planning&lt;/li&gt;
&lt;li&gt;Disaster recovery testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Troubleshooting common issues
&lt;/h3&gt;

&lt;p&gt;High false positive rate&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;# Check current thresholds&lt;/span&gt;
aws dynamodb scan &lt;span class="nt"&gt;--table-name&lt;/span&gt; anomaly-rules

&lt;span class="c"&gt;# Adjust sensitivity&lt;/span&gt;
aws dynamodb update-item &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--table-name&lt;/span&gt; anomaly-rules &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--key&lt;/span&gt; &lt;span class="s1"&gt;'{"ruleId": {"S": "velocity-check"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-expression&lt;/span&gt; &lt;span class="s2"&gt;"SET threshold = :val"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--expression-attribute-values&lt;/span&gt; &lt;span class="s1"&gt;'{":val": {"N": "150"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SageMaker Endpoint Down&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;# Check current endpoint status&lt;/span&gt;
aws sagemaker describe-endpoint &lt;span class="nt"&gt;--endpoint-name&lt;/span&gt; anomaly-detection-endpoint

&lt;span class="c"&gt;# Restart if needed&lt;/span&gt;
aws sagemaker update-endpoint &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-name&lt;/span&gt; anomaly-detection-endpoint &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-config-name&lt;/span&gt; new-config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kinesis Stream Throttling&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;# Check stream metrics&lt;/span&gt;
aws cloudwatch get-metric-statistics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/Kinesis &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; WriteProvisionedThroughputExceeded &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;StreamName,Value&lt;span class="o"&gt;=&lt;/span&gt;event-stream

&lt;span class="c"&gt;# Increase shard count if needed&lt;/span&gt;
aws kinesis update-shard-count &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stream-name&lt;/span&gt; event-stream &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-shard-count&lt;/span&gt; 16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scaling-type&lt;/span&gt; UNIFORM_SCALING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Backup &amp;amp; Disaster Recovery
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Backup Strategy
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;DynamoDB: Point-in-time recovery (PITR) enabled&lt;/li&gt;
&lt;li&gt;S3: Versioning and cross-region replication configured&lt;/li&gt;
&lt;li&gt;Code: Managed via Git with multiple remote repositories&lt;/li&gt;
&lt;li&gt;Configuration: Daily backups of Parameter Store values&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  RTO / RPO Targets
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;RTO (Recovery Time Objective): Less than 4 hours&lt;/li&gt;
&lt;li&gt;RPO (Recovery Point Objective): Less than 15 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Disaster Recovery (DR) procedures
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Activate secondary region&lt;/li&gt;
&lt;li&gt;Restore DynamoDB backups&lt;/li&gt;
&lt;li&gt;Redeploy CloudFormation stacks&lt;/li&gt;
&lt;li&gt;Redirect traffic to the failover stack&lt;/li&gt;
&lt;li&gt;Perform full system validation&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  API Reference
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Event Ingestion
&lt;/h4&gt;

&lt;p&gt;POST /events&lt;br&gt;
Main endpoint used for real-time event ingestion into the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headers&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Type: application/json  
Authorization: AWS4-HMAC-SHA256 ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Request Body&lt;/strong&gt;&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;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unique-event-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-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;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-24T10:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transaction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transactionAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;250.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestsPerSecond"&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;"userAgent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"geoLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"US-East-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;"sessionLength"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ipAddress"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"192.168.1.100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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;"productId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod-456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paymentMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"credit_card"&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;strong&gt;Response&lt;/strong&gt;&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Event received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"unique-event-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-24T10:30:01Z"&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;Error Responses&lt;/strong&gt;&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INVALID_PAYLOAD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Missing required field: userId"&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;h3&gt;
  
  
  Anomaly Query
&lt;/h3&gt;

&lt;p&gt;GET /anomalies&lt;br&gt;
Used to retrieve detected anomalies using optional filters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Parameters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;severity: CRITICAL, HIGH, MEDIUM, LOW&lt;/li&gt;
&lt;li&gt;fromDate: ISO 8601 datetime&lt;/li&gt;
&lt;li&gt;toDate: ISO 8601 datetime&lt;/li&gt;
&lt;li&gt;userId: ID of a specific user&lt;/li&gt;
&lt;li&gt;limit: Max number of results (default: 100)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Example Request&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;-X&lt;/span&gt; GET &lt;span class="s2"&gt;"https://api.anomaly-guardian.com/anomalies?severity=HIGH&amp;amp;limit=50"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;token&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Response Example&lt;/strong&gt;&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;"anomalies"&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;"anomalyId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anom-789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&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;"anomalyScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"detectedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-07-24T10:25:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-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;"anomalyType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"velocity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Unusual request rate detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"originalEvent"&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;...&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;"actionsPerformed"&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="s2"&gt;"rate_limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"enhanced_monitoring"&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;"pagination"&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;"nextToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJ0aW1lc3RhbXAiOiIyMDI1..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hasMore"&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="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;h3&gt;
  
  
  Rule management
&lt;/h3&gt;

&lt;p&gt;Create Rule&lt;br&gt;
POST /rules&lt;br&gt;
Request Body&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;"ruleName"&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-velocity-check"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ruleType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"velocity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Detect high request velocity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&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;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"requestsPerSecond"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"greater_than"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"timeWindow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5m"&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;"actions"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rate_limit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"maxRequestsPerMinute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1h"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notify"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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;"channels"&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="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"severity"&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;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;"enabled"&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="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;Update rule&lt;/strong&gt;&lt;br&gt;
PUT /rules/{ruleId}&lt;br&gt;
Used to update an existing anomaly detection rule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Delete rule&lt;/strong&gt;&lt;br&gt;
DELETE /rules/{ruleId}&lt;br&gt;
Soft delete an existing rule, the rule is deactivated but remains stored.&lt;/p&gt;
&lt;h3&gt;
  
  
  Webhook Configuration
&lt;/h3&gt;

&lt;p&gt;Register Webhook&lt;br&gt;
POST /webhooks&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request Body&lt;/strong&gt;&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://your-system.com/webhooks/anomaly"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&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="s2"&gt;"anomaly.detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anomaly.resolved"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severityFilter"&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="s2"&gt;"CRITICAL"&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;"secret"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"webhook-secret-key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retryPolicy"&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;"maxRetries"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"backoffMultiplier"&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="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;h3&gt;
  
  
  Advanced Use Cases
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;E-commerce Fraud Detection
Scenario:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An online store with over 1 million transactions per day needs to detect fraudulent behavior in real time.&lt;/p&gt;

&lt;p&gt;Specific configuration:&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="na"&gt;Anomaly Rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;High-Value Transactions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;$5,000&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Velocity Attacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;50&lt;/span&gt; &lt;span class="err"&gt;attempts/minute&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Geographic Anomalies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Multiple locations within &amp;lt;1 hour&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Device Fingerprinting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Suspicious device changes&lt;/span&gt;

&lt;span class="na"&gt;Response Actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Automatic Transaction Hold&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Multi-factor Authentication Request&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Manual Review Queue&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Customer Communication&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Success metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;95% reduction in undetected fraud attempts&lt;/li&gt;
&lt;li&gt;Detection time under 2 seconds&lt;/li&gt;
&lt;li&gt;False positive rate below 0.1%&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;API Security Monitoring
Scenario:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protect critical APIs against DDoS attacks and abusive behavior.&lt;/p&gt;

&lt;p&gt;Specific configuration:&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="na"&gt;Protection Layers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Rate Limiting&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;By IP, user, and endpoint&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Behavior Analysis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unusual usage patterns&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Bot Detection&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Based on user-agent and behavioral fingerprinting&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Geo-blocking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restrict access from high-risk countries&lt;/span&gt;

&lt;span class="na"&gt;Auto-Response&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;IP Blacklisting (temporary)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;CAPTCHA Challenge&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Traffic Throttling&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Incident Creation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;IoT Device Monitoring
Scenario:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Monitoring of 100k+ IoT devices to detect anomalies, failures, and potential attacks.&lt;/p&gt;

&lt;p&gt;Specific Configuration:&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="na"&gt;Sensor Metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Temperature Anomalies&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Connectivity Patterns&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Power Consumption&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Data Transmission Rates&lt;/span&gt;

&lt;span class="na"&gt;ML Models&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Time Series Forecasting&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Clustering for Device Behavior&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Outlier Detection&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Predictive Maintenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Machine Learning details
&lt;/h2&gt;

&lt;p&gt;This section outlines the models used for anomaly detection, including feature selection and detection logic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Isolation Forest
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.ensemble&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;IsolationForest&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IsolationForest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;contamination&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# 10% expected anomalies
&lt;/span&gt;    &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;n_estimators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Selected Features
&lt;/span&gt;&lt;span class="n"&gt;features&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;transaction_amount_normalized&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="s"&gt;requests_per_second&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="s"&gt;session_length&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="s"&gt;hour_of_day&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="s"&gt;day_of_week&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="s"&gt;geo_distance_from_usual&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="s"&gt;device_change_indicator&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;ol&gt;
&lt;li&gt;LSTM Autoencoder (For Time Series)
&lt;/li&gt;
&lt;/ol&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;tensorflow&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LSTM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_sequences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input_shape&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timesteps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LSTM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_sequences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RepeatVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timesteps&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LSTM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_sequences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LSTM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_sequences&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TimeDistributed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keras&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Dense&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Anomaly is detected when reconstruction error &amp;gt; threshold
&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reconstruction_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Statistical Process Control (SPC)
&lt;/li&gt;
&lt;/ol&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;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_control_limits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Calculates dynamic control limits based on rolling stats&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rolling_mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rolling_std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rolling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;window_size&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;upper_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rolling_mean&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rolling_std&lt;/span&gt;
    &lt;span class="n"&gt;lower_limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rolling_mean&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rolling_std&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;upper_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lower_limit&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_anomaly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upper_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lower_limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Detects anomaly if value is outside control limits&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;upper_limit&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lower_limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Feature engineering pipeline
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Applied Transformations
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Normalization: Z-score normalization for numerical features.&lt;/li&gt;
&lt;li&gt;Categorical Encoding: One-hot encoding for categorical variables.&lt;/li&gt;
&lt;li&gt;Time Features: Extracted features such as hour, day of week, and month.&lt;/li&gt;
&lt;li&gt;Aggregations: Moving averages, percentiles, counters.&lt;/li&gt;
&lt;li&gt;Geo Features: Distance from typical geolocation patterns.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Feature Selection&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;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.feature_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SelectKBest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_classif&lt;/span&gt;

&lt;span class="c1"&gt;# Select top 20 most relevant features
&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SelectKBest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;f_classif&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_selected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Automatically selected features
&lt;/span&gt;&lt;span class="n"&gt;selected_features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_support&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Model Training pipeline
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Data Pipeline
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;1. Data Extraction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Kinesis Data Streams&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;JSON events&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Volume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1M+ events/day&lt;/span&gt;

&lt;span class="na"&gt;2. Data Preprocessing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Cleaning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Remove duplicates, handle missing values&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Transformation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Apply feature engineering&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Data quality checks&lt;/span&gt;

&lt;span class="na"&gt;3. Feature Store&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SageMaker Feature Store&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Serving&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Real-time and batch&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Monitoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Feature drift detection&lt;/span&gt;

&lt;span class="na"&gt;4. Model Training&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Framework&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SageMaker Training Jobs&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Algorithms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Multiple models ensemble&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Validation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cross-validation + holdout&lt;/span&gt;

&lt;span class="na"&gt;5. Model Evaluation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Precision, Recall, F1, AUC&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Business Metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cost of &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="s"&gt; positives/negatives&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;A/B Testing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Champion/Challenger comparison&lt;/span&gt;

&lt;span class="na"&gt;6. Model Deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Canary deployment&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Blue/green deployment&lt;/span&gt;
   &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Monitoring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Real-time performance tracking&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Continuous Learning
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Model Retraining Strategy
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled Retraining: Weekly retraining with newly collected data.&lt;/li&gt;
&lt;li&gt;Performance-Based: Triggered when evaluation metrics drop below acceptable thresholds.&lt;/li&gt;
&lt;li&gt;Concept Drift Detection: Ongoing monitoring of data distribution and feature drift in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Feedback Loop
&lt;/h4&gt;

&lt;p&gt;Incorporating analyst feedback into model updates allows the system to continuously learn from real-world validation, improving accuracy over time.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update_model_with_feedback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feedback_data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Update model with analyst feedback&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="c1"&gt;# Combine analyst labels with original features
&lt;/span&gt;    &lt;span class="n"&gt;labeled_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;combine_feedback_with_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feedback_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Incremental model retraining
&lt;/span&gt;    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial_fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labeled_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labeled_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Validate new performance
&lt;/span&gt;    &lt;span class="n"&gt;new_performance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;evaluate_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation_set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_performance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;current_performance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;deploy_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;log_model_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_performance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reports &amp;amp; Analytics
&lt;/h3&gt;

&lt;p&gt;Executive Dashboard&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key KPIs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security Posture Score: 0–100 based on multiple metrics.&lt;/li&gt;
&lt;li&gt;Threat Detection Rate: % of threats detected vs. total.&lt;/li&gt;
&lt;li&gt;False Positive Rate: % of alerts that were false positives.&lt;/li&gt;
&lt;li&gt;Mean Time to Detection: Average time to detect anomalies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Visualizations:&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;"widgets"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scorecard"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Security Score"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;94&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"trend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+2% from last week"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"timeseries"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anomalies Over Time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hourly_anomaly_counts"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heatmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Anomaly Types by Hour"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anomaly_type_hour_matrix"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"geographic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Geographic Distribution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"anomalies_by_location"&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;h3&gt;
  
  
  Automated reports
&lt;/h3&gt;

&lt;p&gt;Daily Report:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_daily_report&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;report&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;date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summary&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_events_processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_event_count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anomalies_detected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_anomaly_count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actions_performed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_action_count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_performance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_performance_metrics&lt;/span&gt;&lt;span class="p"&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;top_anomalies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_top_anomalies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system_health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;get_health_status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recommendations&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generate_recommendations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;send_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recipients&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;security-team@company.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;store_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3_bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reports-bucket&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;p&gt;&lt;strong&gt;Weekly report:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trend analysis&lt;/li&gt;
&lt;li&gt;ML model performance&lt;/li&gt;
&lt;li&gt;Threat evolution&lt;/li&gt;
&lt;li&gt;Adjustment recommendations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monthly report:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solution ROI&lt;/li&gt;
&lt;li&gt;Industry benchmarks&lt;/li&gt;
&lt;li&gt;Capacity planning&lt;/li&gt;
&lt;li&gt;Improvement roadmap&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD Pipeline
&lt;/h3&gt;

&lt;p&gt;Infrastructure as Code&lt;/p&gt;

&lt;h4&gt;
  
  
  GitOps Workflow:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy.yml&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;Deploy Anomaly Guardian&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;actions/checkout@v3&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;Validate CloudFormation&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;aws cloudformation validate-template \&lt;/span&gt;
            &lt;span class="s"&gt;--template-body file://anomaly-guardian-base.yaml&lt;/span&gt;
          &lt;span class="s"&gt;aws cloudformation validate-template \&lt;/span&gt;
            &lt;span class="s"&gt;--template-body file://anomaly-guardian-advanced.yaml&lt;/span&gt;

  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;actions/checkout@v3&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;Run Tests&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;python -m pytest tests/&lt;/span&gt;
          &lt;span class="s"&gt;python -m pytest integration_tests/&lt;/span&gt;

  &lt;span class="na"&gt;security-scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;actions/checkout@v3&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;Security Scan&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;checkov -f anomaly-guardian-base.yaml&lt;/span&gt;
          &lt;span class="s"&gt;cfn-lint anomaly-guardian-advanced.yaml&lt;/span&gt;

  &lt;span class="na"&gt;deploy-staging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;security-scan&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Deploy to Staging&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;./deploy.sh deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;

  &lt;span class="na"&gt;integration-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy-staging&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Run Integration Tests&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;./deploy.sh test&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;staging&lt;/span&gt;

  &lt;span class="na"&gt;deploy-production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integration-tests&lt;/span&gt;
    &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.ref == 'refs/heads/main'&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&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;Deploy to Production&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;./deploy.sh deploy&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Testing Strategy
&lt;/h4&gt;

&lt;p&gt;Unit Tests&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;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anomaly_detector&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AnomalyDetector&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestAnomalyDetection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setup_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AnomalyDetector&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_velocity_anomaly_detection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;event&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;userId&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="s"&gt;test-user&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="s"&gt;requestsPerSecond&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&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="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_anomaly&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anomaly_type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;velocity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_normal_behavior&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;event&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;userId&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="s"&gt;test-user&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="s"&gt;requestsPerSecond&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&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="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_anomaly&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integration Tests&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;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_end_to_end_anomaly_detection&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Send an anomalous event
&lt;/span&gt;    &lt;span class="n"&gt;kinesis&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;kinesis&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="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_anomalous_event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;kinesis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;StreamName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anomaly-guardian-test-event-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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="n"&gt;PartitionKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;userId&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="c1"&gt;# Wait for processing
&lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Verify if anomaly was stored
&lt;/span&gt;    &lt;span class="n"&gt;dynamodb&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;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;anomaly-guardian-test-anomaly-history&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Key&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;anomalyId&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventId&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="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&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="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HIGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Performance Tests&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;concurrent.futures&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_throughput&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Test maximum system throughput&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_event&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="c1"&gt;# Send event via API Gateway
&lt;/span&gt;        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;test_event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

    &lt;span class="c1"&gt;# Run test with 1000 concurrent events
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;send_event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;success_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;success_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;  &lt;span class="c1"&gt;# 99% success rate
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost Estimation - Production Environment (2M events/day)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compute&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
AWS Lambda: Responsible for processing incoming events. Estimated cost is $45/month, considering invocation volume and average execution time.&lt;br&gt;&lt;br&gt;
SageMaker Endpoint: Hosts real-time ML inference. Due to the need for a persistent endpoint, the cost is relatively higher, around $180/month.&lt;br&gt;&lt;br&gt;
Step Functions: Manages orchestration between different stages of the processing flow. The estimated cost is $25/month.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
DynamoDB: Used to store anomaly history and related metadata. With high read/write throughput, cost is estimated at $120/month.&lt;br&gt;&lt;br&gt;
S3 (Simple Storage Service): Stores logs, backups, and reports. Assuming 1 TB of monthly traffic, storage costs are around $35/month.&lt;br&gt;&lt;br&gt;
Elasticsearch (OpenSearch): Powers fast search and analytics on event data. With high availability enabled, estimated cost is $200/month.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Streaming&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Kinesis Data Streams: Handles real-time event ingestion. Based on provisioned throughput, the cost is $150/month.&lt;br&gt;&lt;br&gt;
Kinesis Data Analytics: Runs real-time SQL queries on incoming streams. This service contributes approximately $100/month.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Networking&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
API Gateway: Serves HTTP endpoints to receive and route events. For high-volume access, the estimated monthly cost is $35.&lt;br&gt;&lt;br&gt;
Data Transfer: Covers outbound data (egress) from AWS. Estimated cost is $65/month, assuming standard internet traffic volumes.  &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Total Monthly Estimate:&lt;/strong&gt; US$955/month&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost Optimization Strategies:
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Reserved Instances: 40% savings on SageMaker
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Purchase Reserved Instance for SageMaker&lt;/span&gt;
aws sagemaker put-reserved-capacity &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--reserved-capacity-offering-id&lt;/span&gt; &lt;span class="s2"&gt;"offering-123"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-count&lt;/span&gt; 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;DynamoDB On-Demand: Savings for variable workloads
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;BillingMode: PAY_PER_REQUEST  &lt;span class="c"&gt;# vs PROVISIONED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;S3 Intelligent Tiering: Automatic storage cost savings
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;StorageClass: INTELLIGENT_TIERING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Lambda Provisioned Concurrency: Optimization for cold starts
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ProvisionedConcurrency: 10  &lt;span class="c"&gt;# For critical functions only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cost Monitoring
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Budget alerts
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;MonthlyBudget&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Amount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt;
  &lt;span class="na"&gt;Unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;USD&lt;/span&gt;
  &lt;span class="na"&gt;Alerts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;80%&lt;/span&gt;
      &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ACTUAL&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100%&lt;/span&gt;
      &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FORECASTED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Cost Anomaly Detection:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;detect_cost_anomalies&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Detect anomalies in AWS costs&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;ce&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;ce&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_anomalies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;DateInterval&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;StartDate&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="s"&gt;2025-07-01&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="s"&gt;EndDate&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="s"&gt;2025-07-24&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;anomaly&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Anomalies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;anomaly&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Impact&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="s"&gt;TotalImpact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Trigger alert
&lt;/span&gt;            &lt;span class="nf"&gt;send_cost_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anomaly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Advanced Security Features
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Encryption Everywhere
&lt;/h4&gt;

&lt;p&gt;Data Classification:&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="na"&gt;Sensitive Data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;User IDs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pseudonymized&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;IP Addresses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hashed&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Transaction Amounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Encrypted&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Geographic Data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generalized&lt;/span&gt;

&lt;span class="na"&gt;Public Data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Aggregate Statistics&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;System Performance Metrics&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Non-PII Event Metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key Management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KMS Keys:
  Application Key:
    Description: "Anomaly Guardian Application Encryption"
    KeyRotation: Enabled
    Aliases: ["alias/anomaly-guardian-app"]

  Database Key:
    Description: "DynamoDB Encryption Key"
    KeyRotation: Enabled
    Aliases: ["alias/anomaly-guardian-db"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Network Security
&lt;/h4&gt;

&lt;p&gt;VPC Configuration:&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="na"&gt;Network Architecture&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;VPC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10.0.0.0/16&lt;/span&gt;

&lt;span class="na"&gt;Public Subnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.101.0/24 (AZ-1)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.102.0/24 (AZ-2)&lt;/span&gt;

&lt;span class="na"&gt;Private Subnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.1.0/24 (AZ-1)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.2.0/24 (AZ-2)&lt;/span&gt;

&lt;span class="na"&gt;Database Subnets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.201.0/24 (AZ-1)&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;10.0.202.0/24 (AZ-2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security Groups:&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="na"&gt;Lambda Security Group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HTTPS (443) from anywhere&lt;/span&gt;
  &lt;span class="na"&gt;Egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DynamoDB VPC endpoint&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Elasticsearch cluster&lt;/span&gt;

&lt;span class="na"&gt;API Gateway Security Group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HTTPS (443) from anywhere&lt;/span&gt;
  &lt;span class="na"&gt;Egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;None (managed service)&lt;/span&gt;

&lt;span class="na"&gt;Elasticsearch Security Group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Ingress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Port 443 from Lambda SG&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;Port 9200 from Lambda SG&lt;/span&gt;
  &lt;span class="na"&gt;Egress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WAF Protection:&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="na"&gt;WAF Rules&lt;/span&gt;&lt;span class="pi"&gt;:&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWSManagedRulesCommonRuleSet"&lt;/span&gt;
    &lt;span class="na"&gt;Priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ManagedRuleGroupStatement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;VendorName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS"&lt;/span&gt;
        &lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWSManagedRulesCommonRuleSet"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RateLimitRule"&lt;/span&gt;
    &lt;span class="na"&gt;Priority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;RateBasedStatement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Limit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2000&lt;/span&gt;
        &lt;span class="na"&gt;AggregateKeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IP"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When dealing with highly complex environments and high data volumes, the response time between identifying an anomaly and taking action is crucial. More than just a functional architecture, what is presented here is an approach focused on operational reliability, processing scalability, and cost control.&lt;/p&gt;

&lt;p&gt;Each component has been integrated with a purpose: to accurately detect deviations, trigger automated responses, and maintain active governance at all levels from security to resource allocation. This type of solution becomes truly effective when aligned with the organization's technical routine and business objectives. It is not just about technology, but about delivering predictability in scenarios where failures are costly.&lt;/p&gt;

&lt;p&gt;Thank you, see you next time.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>machinelearning</category>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>AWS Copilot Deployment Tutorial: From Monolith to Microservice</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Fri, 20 Jun 2025 18:09:54 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-copilot-deployment-tutorial-from-monolith-to-microservice-5a7d</link>
      <guid>https://dev.to/aws-builders/aws-copilot-deployment-tutorial-from-monolith-to-microservice-5a7d</guid>
      <description>&lt;p&gt;This tutorial (for Mac) will guide you through deploying a Node.js application using AWS Copilot, first as a monolith and then breaking it down into microservices. By following these steps, you'll learn how to use AWS Copilot to deploy containerized applications to Amazon ECS.&lt;br&gt;
I will leave a doc with all systems contemplated. Link: All Systems (&lt;a href="https://docs.google.com/document/d/18jeAk3MAEQak4xyv3kl4HWdV6QiitDZo61rIRmet1xE/edit?usp=sharing" rel="noopener noreferrer"&gt;Linux &amp;amp; Windows&lt;/a&gt;)&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%2Fdaquwf4zw4tx2jcmnxo0.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%2Fdaquwf4zw4tx2jcmnxo0.png" alt="Image description" width="491" height="301"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Tutorial Modules Overview
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Introduction: Understanding the application and architecture&lt;/li&gt;
&lt;li&gt;Setup: Installing and configuring prerequisites&lt;/li&gt;
&lt;li&gt;Containerize and Deploy the Monolith: Building and deploying the application as a monolith&lt;/li&gt;
&lt;li&gt;Break the Monolith: Separating the application into microservices&lt;/li&gt;
&lt;li&gt;Deploy Microservices: Deploying each microservice independently&lt;/li&gt;
&lt;li&gt;Clean Up: Removing all resources to avoid ongoing charges&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;
&lt;h5&gt;
  
  
  Before starting this tutorial, ensure you have the following:
&lt;/h5&gt;

&lt;ol&gt;
&lt;li&gt;AWS Account: You need an active AWS account. Some services may require your account to be active for more than 12 hours after creation.&lt;/li&gt;
&lt;li&gt;Docker: Docker must be installed and running on your system.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  For macOS:
&lt;/h4&gt;

&lt;p&gt;Install Docker Desktop for Mac&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Download from https://www.docker.com/products/docker-desktop/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify Docker installation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker --version
&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%2Fktck0j9qkb0eslh8x8p4.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%2Fktck0j9qkb0eslh8x8p4.png" alt="Image description" width="800" height="60"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install AWS CLI v2 (for macOS)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify installation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws --version
&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%2F6efhn8rzgjve8ttpxx8b.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%2F6efhn8rzgjve8ttpxx8b.png" alt="Image description" width="800" height="62"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Configure AWS credentials&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws configure
&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%2F816rv469qykexd9lttzp.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%2F816rv469qykexd9lttzp.png" alt="Image description" width="800" height="132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS Copilot CLI: Install the AWS Copilot CLI&lt;/p&gt;

&lt;p&gt;Install via Homebrew (macOS)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brew install aws/tap/copilot-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify installation&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot --version
&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%2Fmij91hfysygjg4tkexpz.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%2Fmij91hfysygjg4tkexpz.png" alt="Image description" width="800" height="53"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1: Deploying the Monolithic Application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Create an AWS Copilot Application
&lt;/h3&gt;

&lt;p&gt;An AWS Copilot Application is a collection of services and environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Clone the sample repository
git clone https://github.com/awslabs/amazon-ecs-nodejs-microservices
cd ./amazon-ecs-nodejs-microservices/

# Initialize a new Copilot application
copilot app init
# Name the application "api" when prompted
&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%2Fw7w8i70f0s4s2uc0lxcy.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%2Fw7w8i70f0s4s2uc0lxcy.png" alt="Image description" width="800" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create the Environment
&lt;/h3&gt;

&lt;p&gt;Copilot environments represent the infrastructure where your application runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create a new environment
copilot env init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted:&lt;br&gt;
Name the environment "api"&lt;br&gt;
Choose "profile default" for credentials&lt;br&gt;
Select "Yes, use default" for environment configuration&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%2F84ztd4db9kfb3t40e726.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%2F84ztd4db9kfb3t40e726.png" alt="Image description" width="800" height="257"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Deploy the Environment
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Deploy the environment
copilot env deploy --name api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This step creates the VPC, subnets, security groups, and other necessary infrastructure for your application.&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%2F7lb9pkc31yyxb7lt76bh.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%2F7lb9pkc31yyxb7lt76bh.png" alt="Image description" width="800" height="188"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 4: Create the Monolith AWS Copilot Service
&lt;/h3&gt;

&lt;p&gt;A Copilot Service runs containers on ECS or Fargate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create a new service
copilot svc init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When prompted:&lt;br&gt;
Select "Load Balanced Web Service" as the workload type&lt;br&gt;
Name the service "monolith"&lt;br&gt;
For the Dockerfile path, select or enter the appropriate path to the Dockerfile&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%2Fd7ljq19o7zsfz04d840e.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%2Fd7ljq19o7zsfz04d840e.png" alt="Image description" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Deploy the Monolith Service
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Deploy the API
copilot env deploy --name api
&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%2Fqcgrzen16039acz6ss2s.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%2Fqcgrzen16039acz6ss2s.png" alt="Image description" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Deploy the service
&lt;/h1&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot svc deploy --name monolith
&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%2Fsnyn9krlsejpnli9gust.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%2Fsnyn9krlsejpnli9gust.png" alt="Image description" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This builds the Docker container locally, pushes it to an Amazon Elastic Container Registry (ECR) repository, and deploys it on Amazon Elastic Container Service (ECS).&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%2Ffskov0fqx0x6wdwfji5x.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%2Ffskov0fqx0x6wdwfji5x.png" alt="Image description" width="800" height="289"&gt;&lt;/a&gt;&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%2Fyybkra6ftvrmlfj66het.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%2Fyybkra6ftvrmlfj66het.png" alt="Image description" width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 6: Confirm the Deployment
&lt;/h3&gt;

&lt;p&gt;After deployment completes, AWS Copilot will display a URL for your service. You can test the deployment by accessing the URL in a browser or using curl.&lt;br&gt;
You can test the application deployment by entering queries in a browser, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;http:///api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/users/3
&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%2F393rvhimtvl7whuei4y3.png" alt="Image description" width="800" height="49"&gt;
&lt;/li&gt;
&lt;li&gt;http:///api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/threads/2
&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%2Fbfv0g2xggzms8grhw202.png" alt="Image description" width="682" height="72"&gt;
&lt;/li&gt;
&lt;li&gt;http:///api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/
&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%2F67rb3yfyd5bffuifd0ol.png" alt="Image description" width="800" height="38"&gt;
&lt;/li&gt;
&lt;li&gt;http:///api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/in-thread/1
&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%2Fsqbdkaw81kdpfs5paqdz.png" alt="Image description" width="800" height="65"&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Part 2: Breaking the Monolith into Microservices
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Step 1: Create the Microservices
&lt;/h3&gt;
&lt;h4&gt;
  
  
  Create separate services for posts, threads, and users components.
&lt;/h4&gt;

&lt;p&gt;Create the posts service (copy all the script up to Load Balanced Web Service)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot svc init --app api --dockerfile ./3-microservices/services/posts/Dockerfile --name posts --svc-type "Load Balanced Web Service"
&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%2Fk1vh6uda14pizjyi56t0.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%2Fk1vh6uda14pizjyi56t0.png" alt="Image description" width="800" height="195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the threads service (copy all the script up to Load Balanced Web Service)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot svc init --app api --dockerfile ./3-microservices/services/threads/Dockerfile --name threads --svc-type "Load Balanced Web Service"
&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%2Fa75mb757zzqha8jkhq9c.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%2Fa75mb757zzqha8jkhq9c.png" alt="Image description" width="800" height="199"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the users service (copy all the script up to Load Balanced Web Service)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot svc init --app api --dockerfile ./3-microservices/services/users/Dockerfile --name users --svc-type "Load Balanced Web Service"
&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%2Fso4dd93a1ho8zzfjy3u3.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%2Fso4dd93a1ho8zzfjy3u3.png" alt="Image description" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Edit the Path in the Manifest Files
&lt;/h3&gt;

&lt;h4&gt;
  
  
  AWS Copilot sets the path to a service based on the service name. Edit each microservice manifest to ensure proper routing.
&lt;/h4&gt;

&lt;p&gt;Edit the posts service manifest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano copilot/posts/manifest.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the &lt;em&gt;path&lt;/em&gt; under the &lt;em&gt;http&lt;/em&gt; section to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path: "api/posts"
&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%2Fh63q08tino8dfnsea80z.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%2Fh63q08tino8dfnsea80z.png" alt="Image description" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Repeat for the threads and users services, updating their respective paths to:&lt;/p&gt;

&lt;p&gt;For threads service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano copilot/threads/manifest.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path: "api/threads"
&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%2Fei6lt95i0hvia0we725z.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%2Fei6lt95i0hvia0we725z.png" alt="Image description" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For users service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano copilot/users/manifest.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path: "api/users"
&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%2Fbpo0ddud8d4qj1urar2m.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%2Fbpo0ddud8d4qj1urar2m.png" alt="Image description" width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Deploy the Microservices
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Deploy the posts service&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;copilot svc deploy --name posts
&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%2Fsk91jn0nm3qtzijzgcjx.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%2Fsk91jn0nm3qtzijzgcjx.png" alt="Image description" width="800" height="342"&gt;&lt;/a&gt;&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%2F8ytjbio9787jokma9d2a.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%2F8ytjbio9787jokma9d2a.png" alt="Image description" width="800" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your service is accessible at &lt;em&gt;&lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts&lt;/a&gt;&lt;/em&gt; over the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy the threads service&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;copilot svc deploy --name threads
&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%2Fdwbva90tzeahefwhwgg6.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%2Fdwbva90tzeahefwhwgg6.png" alt="Image description" width="800" height="373"&gt;&lt;/a&gt;&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%2Fv7igmkvn6vr7k0hju1yc.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%2Fv7igmkvn6vr7k0hju1yc.png" alt="Image description" width="800" height="213"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your service is accessible at &lt;em&gt;&lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/threads" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/threads&lt;/a&gt;&lt;/em&gt; over the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy the users service&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;copilot svc deploy --name users
&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%2Fsabsx33u8q1n0n7ji9uy.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%2Fsabsx33u8q1n0n7ji9uy.png" alt="Image description" width="800" height="385"&gt;&lt;/a&gt;&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%2Fosrvfylx20vwzfh4i9ne.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%2Fosrvfylx20vwzfh4i9ne.png" alt="Image description" width="800" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your service is accessible at &lt;em&gt;&lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/users" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/users&lt;/a&gt;&lt;/em&gt; over the internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Shut Down the Monolith
&lt;/h3&gt;

&lt;p&gt;Once all microservices are deployed and working, you can shut down the monolith service.&lt;/p&gt;

&lt;p&gt;Delete the monolith service&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot svc delete --name monolith
&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%2F1kigq4h3xitnmch4s4ny.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%2F1kigq4h3xitnmch4s4ny.png" alt="Image description" width="800" height="108"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Verify the Deployment
&lt;/h3&gt;

&lt;p&gt;Test the deployment by accessing the endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users endpoint: &lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/users/3" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/users/3&lt;/a&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%2F332pm8i3gfgrdq471hb9.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%2F332pm8i3gfgrdq471hb9.png" alt="Image description" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Threads endpoint: &lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/threads/2" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/threads/2&lt;/a&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%2F64ra5a39ohuksi411xa7.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%2F64ra5a39ohuksi411xa7.png" alt="Image description" width="800" height="94"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Posts endpoints:&lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/&lt;/a&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%2Ffwz8iag31s19rowwd9ww.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%2Ffwz8iag31s19rowwd9ww.png" alt="Image description" width="800" height="44"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/in-thread/1" rel="noopener noreferrer"&gt;http://api-ap-Publi-tqekqzwAKyhA-2052337664.us-east-1.elb.amazonaws.com/api/posts/in-thread/1&lt;/a&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%2Flozodhn7l8rznpmsnam1.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%2Flozodhn7l8rznpmsnam1.png" alt="Image description" width="800" height="67"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment on AWS:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VPC:&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%2Fot9p96b7r9ktwyompggx.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%2Fot9p96b7r9ktwyompggx.png" alt="Image description" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECS:&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%2Fr8kl1v4be9rfqwb68mkr.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%2Fr8kl1v4be9rfqwb68mkr.png" alt="Image description" width="800" height="259"&gt;&lt;/a&gt;&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%2F7q11cyv7mb3dc2qp8f6f.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%2F7q11cyv7mb3dc2qp8f6f.png" alt="Image description" width="800" height="260"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ELB:&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%2F2u5s4p37tjcrnls6zk3z.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%2F2u5s4p37tjcrnls6zk3z.png" alt="Image description" width="800" height="131"&gt;&lt;/a&gt;&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%2Faz4micen9skvnsj9t83b.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%2Faz4micen9skvnsj9t83b.png" alt="Image description" width="800" height="373"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Clean Up Resources
&lt;/h3&gt;

&lt;p&gt;Best practice is to delete resources when the tutorial is complete to avoid ongoing charges for running services. This cleanup process will terminate all the resources you created during this tutorial.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Delete the Application
&lt;/h4&gt;

&lt;p&gt;To delete the entire application and all associated resources:&lt;/p&gt;

&lt;p&gt;Delete all resources&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;copilot app delete --name api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Copilot will list all the resources that will be deleted&lt;/li&gt;
&lt;li&gt;You will be asked to confirm the deletion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The command will delete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All services running on Amazon ECS&lt;/li&gt;
&lt;li&gt;The Application Load Balancer&lt;/li&gt;
&lt;li&gt;Amazon Elastic Container Registry repositories&lt;/li&gt;
&lt;li&gt;AWS CloudFormation stacks&lt;/li&gt;
&lt;li&gt;Underlying EC2 instances
This process takes approximately 10 minutes to complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Resources Being Deleted&lt;br&gt;
This cleanup will remove:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Elastic Container Service resources&lt;/li&gt;
&lt;li&gt;Amazon Elastic Container Registry repositories&lt;/li&gt;
&lt;li&gt;Elastic Load Balancer&lt;/li&gt;
&lt;li&gt;Associated CloudFormation stacks&lt;/li&gt;
&lt;li&gt;IAM roles created for your services&lt;/li&gt;
&lt;li&gt;CloudWatch logs&lt;/li&gt;
&lt;li&gt;Any other resources created by AWS Copilot&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%2Fdcox1i0x6wh5h6rj94bb.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%2Fdcox1i0x6wh5h6rj94bb.png" alt="Image description" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thank you a lot! See you soon. I hope you enjoy it!&lt;/p&gt;

</description>
      <category>monolith</category>
      <category>microservices</category>
      <category>aws</category>
      <category>amazonecs</category>
    </item>
    <item>
      <title>AWS Resource Explorer with PrivateLink</title>
      <dc:creator>Carlos Filho</dc:creator>
      <pubDate>Tue, 10 Jun 2025 19:58:28 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-resource-explorer-with-privatelink-32an</link>
      <guid>https://dev.to/aws-builders/aws-resource-explorer-with-privatelink-32an</guid>
      <description>&lt;p&gt;AWS continues to expand its offer of resource visibility with enhanced security. The newest addition to the arsenal is the integration between AWS Resource Explorer and AWS PrivateLink, now available in all commercial regions. This move marks an important step for organizations that need to locate resources broadly, but without giving up private traffic, especially in environments with tighter security restrictions.&lt;/p&gt;

&lt;p&gt;Resource Explorer alone represents a breakthrough in the way administrators view their resources spread across different accounts and regions. Now, with support for PrivateLink, this capability can be achieved within the scope of VPC, without the need to expose calls to the public Internet.&lt;/p&gt;

&lt;p&gt;This type of evolution addresses a sensitive point in multi-account architectures: how to ensure that distributed teams have visibility over the environment without compromising private connectivity standards? The new integration solves precisely this dilemma. It enables broad searches for resources using the Console, CLI, or SDKs, all encapsulated within the private VPC fabric.&lt;/p&gt;

&lt;p&gt;In practice, this means that it is possible to consult resources on an organizational scale, with enhanced security and without relying on public routes. The experience is still accessible via the unified search bar, which maintains operational fluidity even in environments with tighter governance.&lt;/p&gt;

&lt;p&gt;The big advantage here is not just in the discovery itself, but in the way this discovery is made. By keeping traffic within the PrivateLink domain, the risk of exposing sensitive metadata and API calls is drastically reduced. This control is essential for companies that need to meet more demanding compliance requirements, especially in sectors such as finance, healthcare, or government.&lt;/p&gt;

&lt;p&gt;With this update, Resource Explorer takes another leap towards becoming not just an inventory tool, but a central point of contextual visibility, and now more compatible with corporate security policies.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed it! See you next time.&lt;/p&gt;

</description>
      <category>privatelink</category>
      <category>resourceexplorer</category>
      <category>vpc</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
