<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>AWS DevOps Case Studies: Terraform, App Runner, WAF · Remote Since Forever</title>
  <subtitle>AWS DevOps case studies — App Runner deploys, Terraform budgets and WAF, Cognito + S3 direct uploads, and an event-driven AWS architecture.</subtitle>
  <link href="https://blog.clearview.team/tags/aws-devops/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://blog.clearview.team/tags/aws-devops/" />
  <updated>2026-08-20T17:39:48+02:00</updated>
  <id>https://blog.clearview.team/tags/aws-devops/feed.xml</id>
  <author>
    <name>Clearview Team</name>
  </author>
  <entry>
    <title>How a CloudFront Custom Error Page Leaked JWTs to S3: A Case Study</title>
    <link href="https://blog.clearview.team/2026/cloudfront-error-page-jwt-leak-case-study/" />
    <id>https://blog.clearview.team/2026/cloudfront-error-page-jwt-leak-case-study/</id>
    <published>2026-08-13T11:00:00+02:00</published>
    <updated>2026-08-13T11:00:00+02:00</updated>
    <author>
      <name>Nedim Hadzimahmutovic</name>
    </author>
    <summary>A pen-test curl found CloudFront forwarding `Authorization` headers to the S3 error-page bucket, where S3 reflected every JWT back in its XML error response. Two lines of Terraform closed it across sixteen distributions.</summary>
    <content type="html">&lt;p&gt;A client was preparing to put an API behind CloudFront in production. During the final penetration test, one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; request showed that every distribution with custom error pages was reflecting users&apos; JWT tokens in S3 XML error responses.&lt;/p&gt;

&lt;p&gt;Sixteen distributions shared the same copy-pasted Terraform block.&lt;/p&gt;

&lt;p&gt;CloudFront is AWS&apos;s content delivery network (CDN). A JSON Web Token (JWT) is the signed session string the API gives a user after login. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; is the command-line tool we use to send HTTP requests by hand. Three terms are enough to follow the finding.&lt;/p&gt;

&lt;p&gt;CloudFront was handling TLS at the edge, HTTP/2 and HTTP/3 connections, DDoS protection, and branded error pages from a separate S3 bucket. The staging setup had been running for weeks. The distributions were stable, the error pages rendered correctly, and the TLS grades looked good.&lt;/p&gt;

&lt;h2 id=&quot;the-one-line-proof&quot;&gt;The One-Line Proof&lt;/h2&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl https://your-api.example.com/custom_error_pages/502.html &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.YOUR_ACTUAL_TOKEN&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If your response looks like this, you&apos;re vulnerable:&lt;/p&gt;

&lt;div class=&quot;language-xml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;Error&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;InvalidArgument&lt;span class=&quot;nt&quot;&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;Message&amp;gt;&lt;/span&gt;Unsupported Authorization Type&lt;span class=&quot;nt&quot;&gt;&amp;lt;/Message&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ArgumentName&amp;gt;&lt;/span&gt;Authorization&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ArgumentName&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;ArgumentValue&amp;gt;&lt;/span&gt;Bearer eyJhbGciOiJIUzI1NiJ9.YOUR_ACTUAL_TOKEN&lt;span class=&quot;nt&quot;&gt;&amp;lt;/ArgumentValue&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/Error&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Your full JWT (user ID, email, role, expiry, everything in the payload) reflected back in an S3 XML error response. The path exists on practically every CloudFront distribution with custom error pages. Any GET request with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header triggers it, no authentication and no origin failure required.&lt;/p&gt;

&lt;p&gt;S3 reflects any &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; scheme, not just Bearer. Basic auth credentials (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Basic dXNlcjpwYXNzd29yZA==&lt;/code&gt;), API tokens (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Token API_TOKEN_VALUE&lt;/code&gt;), anything non-AWS gets dumped into the XML. If your API uses any of these, the same leak applies.&lt;/p&gt;

&lt;p&gt;Cookies, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-API-Key&lt;/code&gt;, and custom headers are forwarded to S3 too but not reflected in the XML. S3 only complains about the one header it tries to parse.&lt;/p&gt;

&lt;h2 id=&quot;why-did-cloudfront-forward-the-authorization-header-to-s3&quot;&gt;Why did CloudFront forward the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header to S3?&lt;/h2&gt;

&lt;p&gt;A CloudFront distribution sits between users and one or more &lt;strong&gt;origins&lt;/strong&gt; (the actual servers that hold your content). Each origin gets a &lt;strong&gt;behavior&lt;/strong&gt; that tells CloudFront which requests to route there and what to forward along.&lt;/p&gt;

&lt;p&gt;In this pattern, there are two origins behind one distribution:&lt;/p&gt;

&lt;figure class=&quot;post-figure--wide&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/cloudfront-error-page-jwt-leak-case-study/topology.svg&quot; alt=&quot;CloudFront distribution topology: the default behavior routes /api/* to the ALB with all viewer headers (correct), while the ordered behavior routes /custom_error_pages/* to S3 with the same headers (misconfigured), causing the JWT to leak.&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;The API behavior needs viewer headers. The S3 error-page behavior does not.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The API origin is your application: an Application Load Balancer (ALB) in front of Elastic Container Service (ECS) containers, a Lambda function, or an EC2 instance. It needs the viewer&apos;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header to authenticate requests, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content-Type&lt;/code&gt; to parse bodies, and cookies for sessions. Forwarding everything to this origin is correct.&lt;/p&gt;

&lt;p&gt;The error page origin is an S3 bucket containing three static HTML files: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;502.html&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;503.html&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404.html&lt;/code&gt;. It serves the same branded &quot;We&apos;ll be right back&quot; page to every user regardless of who they are.&lt;/p&gt;

&lt;p&gt;It needs nothing from the viewer: not their identity, not their cookies, not their query strings. It just needs CloudFront to ask for a file by path, and it returns the HTML.&lt;/p&gt;

&lt;p&gt;The problem is that both origins were given the same origin request policy: the one designed for the API. So when CloudFront routes a request to the S3 error page bucket, it sends along everything the viewer included, including &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization: Bearer &amp;lt;JWT&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;S3 is not your API. It doesn&apos;t understand Bearer tokens. It tries to interpret the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header as AWS Signature Version 4, fails, and returns an XML error that includes the header value it couldn&apos;t parse. Your JWT is now in the response body.&lt;/p&gt;

&lt;p&gt;The setup looks like this in Terraform (CloudFormation and the console follow the same pattern):&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Origin 1: Your API (ALB, ECS, Lambda, etc.)&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;origin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;domain_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_lb&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;dns_name&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;origin_id&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;api&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Origin 2: S3 bucket with static error pages&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;origin&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;domain_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_s3_bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;error_pages&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;website_endpoint&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;origin_id&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error-pages&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# When the API returns 502, serve the S3 error page instead&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;custom_error_response&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;error_code&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;502&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;response_page_path&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/custom_error_pages/502.html&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Behavior for the API (default): forwards all viewer headers&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;default_cache_behavior&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;target_origin_id&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;api&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;origin_request_policy_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_cloudfront_origin_request_policy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AllViewerExceptHostHeader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Behavior for the error pages: ALSO forwards all viewer headers&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;ordered_cache_behavior&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;path_pattern&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/custom_error_pages/*&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;target_origin_id&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error-pages&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;origin_request_policy_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_cloudfront_origin_request_policy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;AllViewerExceptHostHeader&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ↑ THIS IS THE PROBLEM&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllViewerExceptHostHeader&lt;/code&gt; policy tells CloudFront: &quot;forward every header the viewer sent (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cookie&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;X-Custom-Whatever&lt;/code&gt;) to the origin, except &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Host&lt;/code&gt;.&quot; That&apos;s correct for the API behavior, where your backend needs those headers. It&apos;s catastrophically wrong for the error page behavior, where the origin is an S3 bucket serving static HTML.&lt;/p&gt;

&lt;aside class=&quot;callout&quot;&gt;
  &lt;p&gt;&lt;strong&gt;A note on policy names.&lt;/strong&gt; We call out &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllViewerExceptHostHeader&lt;/code&gt; because that&apos;s what was on the error page behaviors we audited. But AWS&apos;s managed policy &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Managed-AllViewer&lt;/code&gt;&lt;/strong&gt; does the same thing. Any policy with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;header_behavior = &quot;allViewer&quot;&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;header_behavior = &quot;allExcept&quot;&lt;/code&gt; that doesn&apos;t explicitly exclude &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; will trigger the same leak.&lt;/p&gt;

  &lt;p&gt;In CloudFormation, look for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OriginRequestPolicyId&lt;/code&gt; on your error page &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CacheBehavior&lt;/code&gt;. In CDK, check &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;originRequestPolicy&lt;/code&gt; on your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BehaviorOptions&lt;/code&gt;. The policy names and IDs are the same; only the syntax differs.&lt;/p&gt;
&lt;/aside&gt;

&lt;h2 id=&quot;three-ways-the-token-leaks&quot;&gt;Three ways the token leaks&lt;/h2&gt;

&lt;p&gt;The error page path isn&apos;t linked anywhere. No browser navigates to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/custom_error_pages/502.html&lt;/code&gt; during normal use. Three practical routes still reach it.&lt;/p&gt;

&lt;p&gt;The path is trivially discoverable by scanners. It&apos;s in CloudFront&apos;s own documentation examples, and tools like ffuf, feroxbuster, and nuclei include &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/error/&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/custom_error_pages/&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/502.html&lt;/code&gt; in their default wordlists. A scanner that hits the path and sees &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;Code&amp;gt;InvalidArgument&amp;lt;/Code&amp;gt;&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ArgumentName&amp;gt;Authorization&amp;lt;/ArgumentName&amp;gt;&lt;/code&gt; in the XML response can identify the misconfiguration from the shape alone. Our pentest tooling found this one that way.&lt;/p&gt;

&lt;p&gt;A production outage can leak the token without any scanner. When the origin returns 502/503, CloudFront intercepts the error and fetches the S3 error page with the original request&apos;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header attached. The user didn&apos;t navigate to the error page; CloudFront sent them there. Their token leaks through a normal deployment, container restart, or scaling event, visible in the response body to any browser extension, corporate proxy, or compromised CDN edge watching that window.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Path&lt;/th&gt;
      &lt;th&gt;Trigger&lt;/th&gt;
      &lt;th&gt;Result&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Direct access&lt;/td&gt;
      &lt;td&gt;A GET request with an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header reaches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/custom_error_pages/502.html&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;S3 reflects the header in XML. No origin failure is needed.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Origin failure&lt;/td&gt;
      &lt;td&gt;An API request carries a JWT, then the origin returns 502 or 503.&lt;/td&gt;
      &lt;td&gt;CloudFront fetches the S3 error page with the original headers, and the token comes back to the user.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Plain HTTP&lt;/td&gt;
      &lt;td&gt;The error-page behavior uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewer_protocol_policy = &quot;allow-all&quot;&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;A network attacker can read the token without breaking its cryptography.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Most of the distributions we audited had the plain-HTTP setting. The API behavior used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect-to-https&lt;/code&gt;; the error-page behavior was a separate block with separate settings.&lt;/p&gt;

&lt;h2 id=&quot;the-terraform-template-problem&quot;&gt;The Terraform Template Problem&lt;/h2&gt;

&lt;p&gt;We found this in a client&apos;s infrastructure codebase. We audited every CloudFront distribution across their staging and production accounts. The same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ordered_cache_behavior&lt;/code&gt; block for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/custom_error_pages/*&lt;/code&gt; showed up on distribution after distribution, all pointing to the same S3 error pages bucket, all using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllViewerExceptHostHeader&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The block was written once for the API distribution, where it was correct. Then it was copied to every other distribution (SPAs, admin dashboards, static sites, upload CDNs) because it &quot;worked&quot; and nobody questioned whether a static error page bucket needed to receive the viewer&apos;s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Terraform&apos;s copy-paste turned one mistake into a pattern across the whole account.&lt;/p&gt;

&lt;p&gt;A handful of the affected distributions served authenticated traffic (APIs and legacy apps). Those were fully exploitable: every authenticated request that hit a 502 leaked the caller&apos;s JWT. The rest served static frontends that don&apos;t normally receive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; headers, but the direct-access path still worked on all of them.&lt;/p&gt;

&lt;h2 id=&quot;the-chain-that-made-it-critical&quot;&gt;The Chain That Made It Critical&lt;/h2&gt;

&lt;p&gt;The JWT leak alone is a HIGH: you can capture a token and replay it for the remaining access-token lifetime. But during this engagement, we found it chained with something else that pushed it to CRITICAL.&lt;/p&gt;

&lt;p&gt;The API signs session tokens with a JWT secret. On staging, that secret was hardcoded as a trivially guessable value in the ECS task definition&apos;s plaintext environment block. Not in Secrets Manager, not pulled from SSM Parameter Store, just a short, dictionary-word string sitting in Terraform.&lt;/p&gt;

&lt;p&gt;We confirmed the secret remotely without any internal access. The API returns different error messages depending on whether a token&apos;s signature verifies:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Token signed with the guessable secret (correct):&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST https://api.staging.example.com/api/session/token &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Cookie: session=&amp;lt;forged-token-signed-with-guessable-secret&amp;gt;&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# → &quot;Session has been revoked&quot; (signature PASSED, DB hash lookup failed)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Token signed with wrong secret:&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-X&lt;/span&gt; POST https://api.staging.example.com/api/session/token &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Cookie: session=&amp;lt;token-with-wrong-signature&amp;gt;&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# → &quot;Invalid session&quot; (signature FAILED)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That error difference acts as an oracle. &quot;Session has been revoked&quot; means the JWT signature verified successfully; the server moved past cryptographic verification to the database lookup phase. &quot;Invalid session&quot; means the signature check failed. The two code paths are distinguishable remotely.&lt;/p&gt;

&lt;h3 id=&quot;we-proved-the-full-chain-end-to-end&quot;&gt;We proved the full chain end-to-end&lt;/h3&gt;

&lt;p&gt;We registered a test account and signed in. We captured the session token, leaked it through the error page, and validated it offline. Then we replayed it and had the account. All with curl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Sign in.&lt;/strong&gt; We registered a test account and signed in. The server set an httpOnly session cookie. It was a long-lived JWT signed with the guessable secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Leak the token.&lt;/strong&gt; We sent a GET request to the error page path. The session token was in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Authorization&lt;/code&gt; header. The S3 XML error returned it verbatim. Byte for byte, it matched the original.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Validate offline.&lt;/strong&gt; We computed the HMAC-SHA256 signature using the guessable secret. It matched the token&apos;s signature exactly. No server interaction needed. The attacker now knows the token is real, who it belongs to, and when it expires.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Replay.&lt;/strong&gt; We sent the leaked session token to the token endpoint. The server verified the JWT signature (passed) and looked up the token hash (found a real active session). It minted a fresh access token.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Account takeover.&lt;/strong&gt; We used the fresh access token to call the user-profile endpoint. The server returned the full profile: ID, email, name, role, all PII fields. Full account access with no password. All from a token that was never meant to leave the httpOnly cookie jar.&lt;/p&gt;

&lt;figure class=&quot;post-figure--wide&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/cloudfront-error-page-jwt-leak-case-study/attack-chain.svg&quot; alt=&quot;Full attack chain: register a test account, sign in, leak the session JWT through the error page, validate the signature offline with the guessable secret, replay the token to get a fresh access token, call GET /me for full account takeover.&quot; loading=&quot;lazy&quot; /&gt;
  &lt;figcaption&gt;The leak becomes account takeover when the staging JWT secret is guessable.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;We ran the entire chain with curl against a live staging environment. We didn&apos;t need source code access to exploit it. We only needed it to discover the hardcoded secret in Terraform. An external attacker reaches the same outcome by guessing the secret or by intercepting a token through the error page leak.&lt;/p&gt;

&lt;h2 id=&quot;the-public-bucket&quot;&gt;The public bucket&lt;/h2&gt;

&lt;p&gt;The bucket is also enumerable:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl https://your-error-pages-bucket.s3.amazonaws.com/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Returns a full &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListBucketResult&lt;/code&gt; XML with every file, size, ETag, and last-modified timestamp, no authentication required.&lt;/p&gt;

&lt;p&gt;It&apos;s public because the error page origin uses an S3 &lt;strong&gt;website endpoint&lt;/strong&gt;, which forces public read access. OAC (Origin Access Control) only works with the REST API endpoint. The pages themselves are benign HTML, but the bucket is one IAM policy drift away from attacker-writable: a phishing form in a 502 page would be served by your own domain, from your own CloudFront distribution, with your own TLS certificate.&lt;/p&gt;

&lt;h2 id=&quot;the-fix&quot;&gt;The Fix&lt;/h2&gt;

&lt;p&gt;The error page behavior serves static HTML from S3. It doesn&apos;t need the viewer&apos;s auth token, cookies, query strings, or any other header.&lt;/p&gt;

&lt;p&gt;When you omit &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;origin_request_policy_id&lt;/code&gt; entirely, CloudFront sends only the minimum required headers to the origin. That is enough for an S3 website endpoint to fetch a file by path, and there is nothing left for S3 to reflect back.&lt;/p&gt;

&lt;p&gt;The simplest fix is to delete the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;origin_request_policy_id&lt;/code&gt; line from every error page behavior. It shouldn&apos;t have been there in the first place.&lt;/p&gt;

&lt;p&gt;But if you want to be explicit about the intent (and make it harder for someone to re-add a permissive policy later thinking it was accidentally removed), define a policy that forwards nothing:&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_cloudfront_origin_request_policy&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ErrorPagesNoAuth&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ErrorPages-NoAuth&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;comment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Error pages are static HTML, no viewer headers needed&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;headers_config&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;header_behavior&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;none&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;cookies_config&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;cookie_behavior&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;none&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;query_strings_config&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;query_string_behavior&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;none&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Then in every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ordered_cache_behavior&lt;/code&gt; for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/custom_error_pages/*&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;ordered_cache_behavior&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;path_pattern&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/custom_error_pages/*&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;target_origin_id&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;error-pages&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;origin_request_policy_id&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_cloudfront_origin_request_policy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;ErrorPagesNoAuth&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;id&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;viewer_protocol_policy&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;redirect-to-https&quot;&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# ... rest unchanged&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Before fix:&lt;/span&gt;
curl https://your-api.com/custom_error_pages/502.html &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer TEST&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# → &amp;lt;ArgumentValue&amp;gt;Bearer TEST&amp;lt;/ArgumentValue&amp;gt;  ← LEAKED&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# After fix:&lt;/span&gt;
curl https://your-api.com/custom_error_pages/502.html &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer TEST&quot;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# → &amp;lt;!DOCTYPE html&amp;gt;&amp;lt;html&amp;gt;...(your 502 page HTML)...  ← SAFE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;For defense in depth:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Switch the S3 origin from a public website endpoint to a private bucket with OAC. This closes the public listing vulnerability and eliminates the unencrypted HTTP origin protocol.&lt;/li&gt;
  &lt;li&gt;Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;error_caching_min_ttl = 0&lt;/code&gt; so error responses aren&apos;t cached at all.&lt;/li&gt;
  &lt;li&gt;Set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;viewer_protocol_policy = &quot;redirect-to-https&quot;&lt;/code&gt; on the error page behavior so the path can&apos;t be accessed over plain HTTP.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Forwarding headers to error pages &lt;em&gt;is&lt;/em&gt; valid when the origin needs the viewer&apos;s identity: a Lambda@Edge function rendering personalized error pages, a Cognito-gated bucket, or a debugging service logging which user hit the error. In those cases the origin is an application that can safely consume auth headers. If your error page origin is S3, the answer is always &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;header_behavior = &quot;none&quot;&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;what-we-shipped&quot;&gt;What we shipped&lt;/h2&gt;

&lt;p&gt;Sixteen distributions closed. The JWT reflection path is gone across the client&apos;s account, and HTTP-to-HTTPS is enforced on every error-page behavior. The one residual risk (the public S3 website endpoint) is on the follow-up list.&lt;/p&gt;

&lt;h2 id=&quot;how-to-check-if-youre-vulnerable&quot;&gt;How to Check If You&apos;re Vulnerable&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Find your error page path.&lt;/strong&gt; Look for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;custom_error_response&lt;/code&gt; blocks in your CloudFront distribution. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;response_page_path&lt;/code&gt; tells you where the error pages live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Send a request with an auth header.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; https://your-domain.com/custom_error_pages/502.html &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Authorization: Bearer CHECK_THIS_TOKEN&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Check the response.&lt;/strong&gt; If you see &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;ArgumentValue&amp;gt;Bearer CHECK_THIS_TOKEN&amp;lt;/ArgumentValue&amp;gt;&lt;/code&gt; in the XML, you&apos;re vulnerable. If you see your HTML error page, you&apos;re fine. A 403 means the path doesn&apos;t exist or the behavior doesn&apos;t allow GET, which is also fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Check all your distributions.&lt;/strong&gt; If you use the same Terraform module or copy-paste pattern across distributions, check every one. In one engagement we found the majority of distributions sharing the same misconfiguration. The one you check might be fine while a dozen others are leaking.&lt;/p&gt;

&lt;h2 id=&quot;check-every-distribution&quot;&gt;Check every distribution&lt;/h2&gt;

&lt;p&gt;Nobody asked whether a static HTML bucket needed the same headers as an authenticated API. If you&apos;re running CloudFront with custom S3 error pages, spend sixty seconds running the curl command above against each distribution.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A note on responsible testing: every test account created during this engagement was registered with a non-existent &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@example.com&lt;/code&gt; address, used only for curl-based proof-of-concept, and cleaned up after the findings were documented. No real user sessions were intercepted or replayed. The full chain was proven end-to-end using our own credentials against a staging environment with explicit authorization from the infrastructure owner.&lt;/em&gt;&lt;/p&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-scan-your-cloudfront-for-this&quot;&gt;We Could Scan Your CloudFront For This&lt;/h2&gt;

  &lt;p&gt;If your infrastructure has CloudFront distributions with custom S3 error pages and the same Terraform block copy-pasted across environments, one curl command will show whether your team has the same leak. &lt;strong&gt;Clearview Team&lt;/strong&gt; runs the scan across every distribution in your account, walks the full exploitation chain wherever a leaked token gives real access, and hands the fix back as a pull request. No secret is guessed and no live session replayed without your explicit sign-off. Send us the AWS account and we will scope it.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;mailto:info@clearview.team?subject=CloudFront%20audit%20enquiry&quot;&gt;Scope a CloudFront audit →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="aws" />
    <category term="cloudfront" />
    <category term="s3" />
    <category term="terraform" />
    <category term="jwt" />
    <category term="security" />
    <category term="api-security" />
    <category term="web-api-security" />
    <category term="aws-devops" />
    <category term="auth-architecture" />
    <category term="case-study" />
    
  </entry>
  
  <entry>
    <title>An Event-Driven Architecture Case Study: Lessons from a Real-World Application</title>
    <link href="https://blog.clearview.team/2025/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/" />
    <id>https://blog.clearview.team/2025/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/</id>
    <published>2025-04-07T19:32:53+02:00</published>
    <updated>2025-04-07T19:32:53+02:00</updated>
    <author>
      <name>Nedim Hadzimahmutovic</name>
    </author>
    <summary>A decade of Clearview engineering for a confidential awards group, and the event-driven AWS architecture that keeps it observable.</summary>
    <content type="html">&lt;p&gt;Over the past decade, Clearview Team has built and maintained a pair of interconnected platforms for a confidential awards group. This is the event-driven AWS architecture that keeps both systems observable.&lt;/p&gt;

&lt;p&gt;&lt;a class=&quot;pdf-download&quot; href=&quot;https://drive.google.com/file/d/1F3mOTKY91DwKj7qQjOxIX9gAZIHKgA2q/view?usp=drive_link&quot; rel=&quot;external noopener&quot;&gt;
  &lt;span class=&quot;pdf-download__icon&quot; aria-hidden=&quot;true&quot;&gt;
    &lt;svg viewBox=&quot;0 0 24 24&quot; width=&quot;22&quot; height=&quot;22&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.8&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot;&gt;
      &lt;path d=&quot;M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z&quot;&gt;&lt;/path&gt;
      &lt;path d=&quot;M14 3v5h5&quot;&gt;&lt;/path&gt;
      &lt;path d=&quot;M12 12v6&quot;&gt;&lt;/path&gt;
      &lt;path d=&quot;M9 15l3 3 3-3&quot;&gt;&lt;/path&gt;
    &lt;/svg&gt;
  &lt;/span&gt;
  &lt;span class=&quot;pdf-download__body&quot;&gt;
    &lt;span class=&quot;pdf-download__label&quot;&gt;Prefer a downloadable version&lt;/span&gt;
    &lt;span class=&quot;pdf-download__title&quot;&gt;Read the case study as a PDF&lt;/span&gt;
  &lt;/span&gt;
  &lt;span class=&quot;pdf-download__cta&quot;&gt;
    Download &lt;span class=&quot;pdf-download__arrow&quot; aria-hidden=&quot;true&quot;&gt;↓&lt;/span&gt;
  &lt;/span&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;the-client&quot;&gt;The Client&lt;/h4&gt;

&lt;p&gt;A Confidential Non-profit and Awards Group
*Membership Operations, Automated and Simplified *&lt;/p&gt;

&lt;p&gt;At one time managed primarily through Excel spreadsheets and mail-in forms, the client’s membership system was functional but time-intensive for its administrators. With the benefit of an automated, online membership management system, client’s Board of Directors wisely contracted Clearview to combine member-related functions into one web-based platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More than a decade later&lt;/strong&gt;, we’re still working in close partnership with the client to maintain and streamline member processes and relations through a secure, custom-built online portal. We’re continually finding new ways to empower administrators and members alike with rich tools and features, supporting this 501(c)(3) non-profit’s continued growth.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Purpose-built member dashboard&lt;/strong&gt; with quick access to useful member functions including online signup, membership upgrade, and renewal; info &amp;amp; address changes; member-only services and perks; automated password reset &amp;amp; forgotten login recovery; notification preferences.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Powerful administrator dashboard&lt;/strong&gt;, complete with tiered admin privileges, quick member lookup &amp;amp; sorting, bulk member management, numerous import/export options, and detailed reports.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Secure, multi-method payment processing&lt;/strong&gt; with in-dashboard payment logs, receipts, and refund processing.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Newsletter and notification management&lt;/strong&gt; including automated synchronization with MailChimp newsletter and SendGrid transactional email platforms.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scheduled self-maintenance&lt;/strong&gt; and member relations tasks including proactive address validation and expiration notifications.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;the-digital-workflow-platform&quot;&gt;The Digital Workflow Platform&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Annual Intake and Review Workflow&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The client also runs an annual intake-and-review cycle that once involved sorting through over a thousand hand-mailed physical packages. This veritable mountain of material was then shuffled through very extensive multi-stage review and selection processes by hand.&lt;/p&gt;

&lt;p&gt;Since joining the client’s team in 2010, Clearview has systematically brought each of these processes together in a single online platform.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Fully web-based intake process&lt;/strong&gt;, including secure online payment; upload of video clips, documents, and signoff forms; followup intake for supplementary materials.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Automated media verification, storage&lt;/strong&gt;, and transcoding, utilizing Vimeo API integration for high-quality, secure, and familiar playback for reviewers. All media is secured against unauthorized download.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Advanced partner dashboard&lt;/strong&gt;, enabling organizations with many moving parts to delegate intake tasks to multiple designated contributors and divide labor.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Review management&lt;/strong&gt;, including secure reviewer application and selection process; a dedicated review portal with high-quality material review, shortlisting, issue flagging, and discussion features; selection and notification workflows.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Selection management&lt;/strong&gt;, including application and eligibility processes and a secure selection interface.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tight integration&lt;/strong&gt; with the organization’s membership database for permissions verification.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Extensive administration dashboard&lt;/strong&gt;, including tiered admin roles; organization and intake management; category and field management; payment, receipt, and transaction management; flexible sponsor-credits system; deadline management, late fee rules, and exceptions; numerous import/export tools.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Secure, triple-redundant data archival&lt;/strong&gt; following each year’s cycle.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;On-call support&lt;/strong&gt;, including email support for contributors, reviewers, and participants.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;the-project&quot;&gt;The Project&lt;/h3&gt;
&lt;p&gt;Clearview Team&apos;s engineers and clients are spread across time zones, so monitoring and incident response carry real coordination overhead. This engagement is where we chose &lt;strong&gt;Event-driven architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;challenges&quot;&gt;Challenges&lt;/h4&gt;

&lt;p&gt;The challenges we aim to solve with this approach:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Decoupling of services:&lt;/strong&gt; To improve the agility of the application we designed our system not to be tightly coupled.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Real-time responsiveness:&lt;/strong&gt; The application services operate on event-driven communication, which means the services can produce an event in real time. This enables our on-call engineers to react to important events that are produced by either user actions or system events.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Improved observability:&lt;/strong&gt; The event-driven approach improves system observability. By analyzing event flows, we constantly monitor and improve the system behavior, identify bottlenecks, and locate the root cause of issues more effectively.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;deployment&quot;&gt;Deployment&lt;/h4&gt;

&lt;p&gt;The application is deployed on the AWS Cloud platform as AWS already provides all the communication buses that we need to produce, broker, and consume events.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Event Types&lt;/em&gt;
We have covered the most important event types in the app, such as:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Finance events&lt;/li&gt;
  &lt;li&gt;Membership events&lt;/li&gt;
  &lt;li&gt;Scheduled job events&lt;/li&gt;
  &lt;li&gt;Health check events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;goals&quot;&gt;Goals&lt;/h4&gt;

&lt;p&gt;This project aims to solve the client’s requirements to create and continuously improve the membership and awards system while implementing a modern system architecture. More details about what kind of problems the application is designed to solve can be seen in the diagram below.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/cover.svg&quot; alt=&quot;Editorial cover — three event sources (submit, vote, payment) fan into an AWS EventBridge bus, which fans out to a Lambda, an SQS queue, and a Kinesis stream.&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;system-architecture&quot;&gt;System Architecture&lt;/h3&gt;
&lt;p&gt;This application is built around separate services, each running inside its own container. Services communicate through events — state changes that trigger actions elsewhere in the system.&lt;/p&gt;

&lt;h4 id=&quot;event-driven-architecture&quot;&gt;Event-driven Architecture&lt;/h4&gt;
&lt;p&gt;An event is a state change — a user placing an order, a payment failing, a scheduled job firing. Services produce events and other services consume them, without needing to know about each other directly.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Event-driven architecture is often called &lt;strong&gt;EDA&lt;/strong&gt; for short.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest of this post covers how we wired the event bus for job scheduling and monitoring — the piece that lets our team get &lt;strong&gt;notified in real-time&lt;/strong&gt; and react before downtime reaches users.&lt;/p&gt;

&lt;h4 id=&quot;event-driven-architecture-showcase&quot;&gt;Event-driven Architecture Showcase&lt;/h4&gt;

&lt;p&gt;Below is a diagram showing what a typical &lt;strong&gt;EDA&lt;/strong&gt; system looks like and how it functions.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_JlwE8qJp69RxLHu9q51GgQ.png&quot; width=&quot;1024&quot; height=&quot;812&quot; alt=&quot;AWS based Event-driven architecture&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS based Event-driven architecture&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;the-application&quot;&gt;The Application&lt;/h3&gt;
&lt;p&gt;The application is built on &lt;strong&gt;NodeJS&lt;/strong&gt; with an API service serving multiple front-end domains. This approach has served us well. We have been running the app as a container on AWS.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_2noPlSZ42q4dacE1fg34HQ.png&quot; width=&quot;1024&quot; height=&quot;298&quot; alt=&quot;The diagram shows API and the DB secured and isolated in the VPC. Frontend is served to the client via CloudFront which pulls the website assets from the S3 bucket.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows API and the DB secured and isolated in the VPC. Frontend is served to the client via CloudFront which pulls the website assets from the S3 bucket.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;blockquote&gt;
  &lt;p&gt;Please keep in mind that the client communicates with the API after it has been loaded on the client-side rendered website.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;event-producers&quot;&gt;Event Producers&lt;/h3&gt;
&lt;p&gt;Events are generated from various services and can be produced either by a client action or by a scheduled job.&lt;/p&gt;

&lt;h4 id=&quot;api-event-producer&quot;&gt;API Event Producer&lt;/h4&gt;

&lt;p&gt;The best example of a client-produced event is any type of event related to credit card payments. The app uses the “@aws-sdk/client-eventbridge” library to communicate with the AWS Event Bridge service.&lt;/p&gt;

&lt;h4 id=&quot;api-events-list&quot;&gt;API Events List&lt;/h4&gt;

&lt;p&gt;The core services of the app produce events. Below is a list of services with a description of what each does.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_r3U4h2KgQ9b15_tqA7BKAA.png&quot; width=&quot;1024&quot; height=&quot;500&quot; alt=&quot;The diagram shows API Services and Controllers that send events.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows API Services and Controllers that send events.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;api-produced-sample-events&quot;&gt;API Produced Sample Events&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Finance Related Events&lt;/em&gt;
Below you can find payment processing failed sample events&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_xAgJylpdUNi2_yDVzKGskg.png&quot; width=&quot;1024&quot; height=&quot;1166&quot; alt=&quot;Health Check Events&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;Health Check Events&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_O0IsEOLwk9HGZcbu5a8cWA.png&quot; width=&quot;818&quot; height=&quot;1222&quot; alt=&quot;Mailing List Events&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;Mailing List Events&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_s8Nk7t3gSSio5cGshNekXQ.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;scheduler-event-producer&quot;&gt;Scheduler Event Producer&lt;/h4&gt;

&lt;p&gt;As it was not necessary to &lt;strong&gt;re-implement the wheel&lt;/strong&gt; and create our scheduling service, we relied on the well-tested and proven &lt;em&gt;AWS EventBridge Scheduler&lt;/em&gt; for the scheduling system. For this purpose a custom and dedicated Event Bus was created and named Client Production Scheduler Bus.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;In this case, the Event Producer is the Scheduler itself.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_ToHAJzE0Jw55CmjYVPy86w.png&quot; width=&quot;1024&quot; height=&quot;934&quot; alt=&quot;The diagram shows events that are sent from the first event producer which is the Event Bridge Scheduler received and processed by The Production Event Bus, parsed by AWS Lambda, and at the end sent to a Slack webhook.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows events that are sent from the first event producer which is the Event Bridge Scheduler received and processed by The Production Event Bus, parsed by AWS Lambda, and at the end sent to a Slack webhook.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;As per the diagram every cron job has the following flow:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;a scheduler&lt;/strong&gt; of recurring type that produces an event and sends it to the custom scheduler bus,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;an endpoint&lt;/strong&gt; that is defined as part of the &lt;strong&gt;api_destination&lt;/strong&gt; event rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;evaluated-jobs&quot;&gt;Evaluated Jobs&lt;/h4&gt;

&lt;p&gt;The complete list can be found below.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_7bEmP87hPbCpHoFooiA_yQ.png&quot; width=&quot;1024&quot; height=&quot;447&quot; alt=&quot;The diagram shows the list of Scheduled jobs, their endpoints, and json payload event they generate which gets sent to the Event Bus.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows the list of Scheduled jobs, their endpoints, and json payload event they generate which gets sent to the Event Bus.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;company-statistic-scheduled-job&quot;&gt;Company Statistic Scheduled Job&lt;/h4&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_HWvN5-hrVkBoB_dMloEovQ.png&quot; width=&quot;1024&quot; height=&quot;716&quot; alt=&quot;The diagram shows events that are produced the Event Bridge Scheduler, received and processed by The Production Event Bus, and forwarded to the target which is unique endpoint via API Destination.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows events that are produced the Event Bridge Scheduler, received and processed by The Production Event Bus, and forwarded to the target which is unique endpoint via API Destination.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Event Scheduler Job Notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Recurring schedule type&lt;/li&gt;
  &lt;li&gt;Cron expression style&lt;/li&gt;
  &lt;li&gt;Runs every 8 hours&lt;/li&gt;
  &lt;li&gt;Target: Scheduler Bus&lt;/li&gt;
  &lt;li&gt;API PutEvents&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;event-rule&quot;&gt;Event Rule&lt;/h4&gt;

&lt;p&gt;The bus receives the event and triggers the api cron company statistic rule.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_j163Hp8a2tgsdighb-ZNcg.png&quot; width=&quot;1024&quot; height=&quot;395&quot; alt=&quot;API Destination&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;API Destination&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_v4C4WRHUZntsaZPQjN2G9w.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-infrastructure&quot;&gt;The Infrastructure&lt;/h3&gt;
&lt;p&gt;In this section, we will cover how the AWS infrastructure was set up, the services that send events to the default Event Bus, and how we process and consume those events.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;The AWS Account&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We used AWS Organizations to separate Production and Development environments. This is important as infrastructure-related events are sent to the default event bus. We wanted production and development events not to trigger the same rules as this could lead to unexpected behaviors.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_Sl6mV4_BKq-9Fh4AcM3jtw.png&quot; width=&quot;1024&quot; height=&quot;696&quot; alt=&quot;The diagram shows how we separated production and development accounts while managing both with a single management account.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows how we separated production and development accounts while managing both with a single management account.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;It makes it easier to have two environments not mixing up as we know for a fact that development events will not end up on the production Event Bus causing potential problems.&lt;/p&gt;

&lt;h4 id=&quot;aws-rds&quot;&gt;AWS RDS&lt;/h4&gt;

&lt;p&gt;The database service’s health and availability are very important for the app’s reliability. Therefore, we have a setup of monitoring database events with Slack notifications.&lt;/p&gt;

&lt;p&gt;Every time there is an event such as the database service backing up an event is created and sent to the Event Bus. Such an event will end up as a Slack notification and be read by our team.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_OiFXhQsnZMFppyx3zlBL6Q.png&quot; width=&quot;1024&quot; height=&quot;759&quot; alt=&quot;The diagram shows events that are sent from the MariaDB service, received and processed by AWS EventBridge, parsed by AWS Lambda, and at the end sent to a Slack webhook.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows events that are sent from the MariaDB service, received and processed by AWS EventBridge, parsed by AWS Lambda, and at the end sent to a Slack webhook.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;blockquote&gt;
  &lt;p&gt;We decided that the following event categories will be forwarded to Slack: Notification, availability, backup, failure, low storage, maintenance and recovery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;the-eventbridge-rule&quot;&gt;The EventBridge Rule&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_uLTdeDSNNGkcKN1UOCqy0g.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;aws-health&quot;&gt;AWS Health&lt;/h4&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_609A4DS0lb9LF2DCiAmZdg.png&quot; width=&quot;1024&quot; height=&quot;685&quot; alt=&quot;The diagram shows events that are sent from the AWS Health service, received and processed by AWS EventBridge, parsed by AWS Lambda, and at the end sent to a Slack webhook.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;The diagram shows events that are sent from the AWS Health service, received and processed by AWS EventBridge, parsed by AWS Lambda, and at the end sent to a Slack webhook.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Events type can be one of the following categories:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;accountNotification&lt;/li&gt;
  &lt;li&gt;issue&lt;/li&gt;
  &lt;li&gt;scheduledChange&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS Health events are sent to the &lt;strong&gt;default&lt;/strong&gt; Event Bus. We want to react when we receive an event from the AWS Health service, therefore we create rules. For example, you can use AWS Health to receive email notifications if you have AWS resources in your AWS account scheduled for updates, such as EC2 instances. Below you can find examples of such rules.&lt;/p&gt;

&lt;h4 id=&quot;ec2-service&quot;&gt;EC2 service&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_mkqrarvjHW-AOxG7ZA7XZQ.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;sample-rules&quot;&gt;Sample rules&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Trigger on Every Event&lt;/em&gt;&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_hymOfaBeob4yFAGwYVkpPg.png&quot; width=&quot;1024&quot; height=&quot;306&quot; alt=&quot;Trigger on Specific Service-related Event&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;Trigger on Specific Service-related Event&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Rule for a specific service and event type category.&lt;/p&gt;

&lt;p&gt;In this example, we will create a rule so that EventBridge reacts to the following.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_QtFrPLM5IxieQQHNR99hUQ.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;multiple-services-and-event-type-categories&quot;&gt;Multiple Services and Event Type Categories&lt;/h4&gt;

&lt;p&gt;The examples in the previous procedure show you how to create a rule for a single service and event type category. You can also create a rule for multiple services and event-type categories. This means that you don’t have to create a separate rule for each service and category that you want to monitor. To do so, you must edit the event pattern as per following example&lt;/p&gt;

&lt;h4 id=&quot;example-rule&quot;&gt;Example Rule&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_3WDUut-SEV5rkMaP2QIw-A.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/an-event-driven-architecture-case-study-lessons-from-a-real-world-application/1_3ZKR4sD7DNN-ZyIejrP2ng.png&quot; alt=&quot;Event-driven architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-architect-your-event-flow&quot;&gt;We Could Architect Your Event Flow&lt;/h2&gt;

  &lt;p&gt;If you are scaling a product whose data flow is starting to outgrow request/response, or if the integrations between your services are getting harder to reason about, this is exactly the kind of engagement &lt;strong&gt;Clearview Team&lt;/strong&gt; takes on. We design event-driven architectures on AWS — EventBridge, SQS, Lambda, Step Functions — with the observability and replay story baked in from day one.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;/work-with-us/&quot;&gt;Scope an engagement →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="application-development" />
    <category term="event-driven-architecture" />
    <category term="case-study" />
    <category term="system-architecture" />
    <category term="aws-devops" />
    
  </entry>
  
  <entry>
    <title>The most Flexible AWS Savings Plan</title>
    <link href="https://blog.clearview.team/2024/the-most-flexible-aws-savings-plan/" />
    <id>https://blog.clearview.team/2024/the-most-flexible-aws-savings-plan/</id>
    <published>2024-03-11T01:12:09+01:00</published>
    <updated>2024-03-11T01:12:09+01:00</updated>
    <author>
      <name>Clearview</name>
    </author>
    <summary>How AWS Compute Savings Plans can cut up to 66% off the cloud bill — without locking you into an instance family.</summary>
    <content type="html">&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/cover.svg&quot; alt=&quot;Editorial cover — a downward-trending Savings Plan line against a flat on-demand baseline, with an &amp;quot;up to 66% off&amp;quot; callout.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;AWS provides several ways to reduce your cost but no other program is as flexible as the &lt;strong&gt;Compute Saving Plan&lt;/strong&gt;. This article covers what you need to know to successfully and efficiently use the AWS savings program.&lt;/p&gt;

&lt;h3 id=&quot;what-are-savings-plans&quot;&gt;What are Savings Plans&lt;/h3&gt;

&lt;p&gt;Put plainly, all AWS cost-saving plans offer cost reductions compared to On-Demand prices, in exchange for a long-term commitment. That commitment is &lt;strong&gt;&lt;em&gt;1 or 3 years long&lt;/em&gt;&lt;/strong&gt; and is an &lt;strong&gt;hourly spend commitment&lt;/strong&gt; type.&lt;/p&gt;

&lt;p&gt;AWS provides three types of Savings Plans:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Compute Savings Plans,&lt;/li&gt;
  &lt;li&gt;EC2 Instance Savings Plans, and&lt;/li&gt;
  &lt;li&gt;Amazon SageMaker Savings Plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The focus of this article is on the most flexible type of savings plan which is the Compute Savings Plan.&lt;/p&gt;

&lt;h3 id=&quot;research-research-research&quot;&gt;Research, Research, Research&lt;/h3&gt;

&lt;p&gt;The first step we need to take is to see if there are any &lt;a href=&quot;https://us-east-1.console.aws.amazon.com/cost-management/home#/savings-plans/recommendations&quot;&gt;Recommendations&lt;/a&gt;. This page might tell you what AWS recommends to purchase. Not everyone gets this kind of suggestion as they are based on your usage.&lt;/p&gt;

&lt;h4 id=&quot;follow-the-next-example-to-understand-how-to-purchase-a-compute-savings-plan&quot;&gt;Follow the next example to understand how to purchase a Compute Savings Plan.&lt;/h4&gt;

&lt;h3 id=&quot;a-simple-example-get-a-discount-on-a-single-t4gmicro-ec2-instance&quot;&gt;A Simple Example: Get a Discount on a Single t4g.micro EC2 Instance&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/purchase-illustration.svg&quot; alt=&quot;Two paths from a single t4g.micro instance in eu-west-1 — Path A on-demand at $0.0092/hour, Path B 1-year Compute Savings Plan at $0.0075/hour (an 18% reduction), with the 3-year term shown as a dotted Path B′ at 42%.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This is a straightforward example. In this example, the commitment is for 1 year in Europe (Ireland), the eu-west-1 region.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A very important note is to know that discounts vary from Region to Region so do your research beforehand.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;step-1-check-your-bill&quot;&gt;Step 1: Check Your Bill&lt;/h3&gt;

&lt;p&gt;Find your bill in the AWS console. You can see something like this.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_bOFhgAIo4NxFWIEB.png&quot; width=&quot;462&quot; height=&quot;83&quot; alt=&quot;AWS Bill screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Bill screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;We identified that we are paying On-Demand prices for a &lt;strong&gt;&lt;em&gt;t4g.micro&lt;/em&gt;&lt;/strong&gt; EC2 instance.&lt;/p&gt;

&lt;h3 id=&quot;step-2-identify-the-savings-rate-in-your-region&quot;&gt;Step 2: Identify the Savings Rate in Your Region&lt;/h3&gt;

&lt;p&gt;Find out in which region your EC2 instance is hosted. After that access the table at &lt;a href=&quot;https://aws.amazon.com/savingsplans/compute-pricing/&quot;&gt;Savings Plan Compute Pricing&lt;/a&gt; page. That page will show you a table like the one below.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_G2yKI4l2pHLtw77n.png&quot; width=&quot;1024&quot; height=&quot;619&quot; alt=&quot;AWS Savings Plan Compute Pricing Page screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Savings Plan Compute Pricing Page screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The goal is to get a discount on the usage of a &lt;strong&gt;&lt;em&gt;t4g.micro&lt;/em&gt;&lt;/strong&gt; EC2 instance.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;On-Demand rate — $0.0092 * 720 hours (hours in a 30-day month) = $6.624&lt;/li&gt;
  &lt;li&gt;Savings Plans rate — $0.0075 * 720 hours (hours in a 30-day month) = $5.62500&lt;/li&gt;
  &lt;li&gt;Savings over On-Demand — 18%&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;If you commit to a&lt;/strong&gt; 3-year term &lt;strong&gt;the savings will be 42% instead of 18% for the Ireland Region.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;step-3-purchase-a-plan&quot;&gt;Step 3: Purchase a Plan&lt;/h3&gt;

&lt;p&gt;So the purchase plan will look like this.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0__Qh-mEqhQjv5IOiZ.png&quot; width=&quot;729&quot; height=&quot;770&quot; alt=&quot;AWS Savings Plan Purchase screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Savings Plan Purchase screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_gXSAULqlrwI8ktBg.png&quot; width=&quot;1024&quot; height=&quot;354&quot; alt=&quot;AWS Savings Plan Cart screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Savings Plan Cart screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Important notes:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Do not purchase large plans and over-commit.&lt;/li&gt;
  &lt;li&gt;Purchase small plans and then monitor the coverage.&lt;/li&gt;
  &lt;li&gt;If you need more coverage at a later period in time you can purchase additional savings plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;The hourly commitment rate is the one after the discount — the Savings Plans Rate.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;reports-analysis&quot;&gt;Reports Analysis&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/reports-illustration.svg&quot; alt=&quot;Two dashboard panels — left, a utilization gauge near full; right, a coverage line climbing from flat-low to about 43% after the plan activates.&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;utilization-report&quot;&gt;Utilization Report&lt;/h4&gt;

&lt;p&gt;Below you can see a sample utilization report for a single day after the savings plan has been purchased.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_V3Hn5IthXeSXfWbN.png&quot; width=&quot;1024&quot; height=&quot;215&quot; alt=&quot;AWS Utilization report screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Utilization report screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;coverage-reports&quot;&gt;Coverage Reports&lt;/h4&gt;

&lt;p&gt;Under the “Billing and Cost Management / Savings Plan” menu, you can find the &lt;a href=&quot;https://us-east-1.console.aws.amazon.com/cost-management/home#/savings-plans/coverage?&quot;&gt;Coverage report&lt;/a&gt;. Here you can get information about how much of your spending has been covered by a savings plan during a certain period. You get information such as &lt;strong&gt;Average coverage&lt;/strong&gt;, &lt;strong&gt;Potential monthly savings vs On-Demand,&lt;/strong&gt; and &lt;strong&gt;On-Demand spend not covered&lt;/strong&gt;.&lt;/p&gt;

&lt;h4 id=&quot;before-creating-a-savings-plan&quot;&gt;Before Creating a Savings Plan&lt;/h4&gt;

&lt;p&gt;This is an example of a Coverage report showing no active Savings plan. It has yet to be activated.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_JKO2iox2GVks_pJ0.png&quot; width=&quot;1024&quot; height=&quot;215&quot; alt=&quot;AWS Coverage report screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Coverage report screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;after-creating-a-savings-plan&quot;&gt;After Creating a Savings Plan&lt;/h4&gt;

&lt;p&gt;This is an example of a Coverage report that shows an active Savings plan with a coverage of 43%&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/0_wl5oFMKLOn4OWYLZ.png&quot; width=&quot;1024&quot; height=&quot;215&quot; alt=&quot;AWS Coverage report screenshot made by the author.&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Coverage report screenshot made by the author.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;blockquote&gt;
  &lt;p&gt;It is very difficult to get 100% coverage so start with 50% and iterate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;do-your-research-first&quot;&gt;Do Your Research First&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-most-flexible-aws-savings-plan/due-diligence-illustration.svg&quot; alt=&quot;A four-card checklist — confirm the regional rate, model the hourly commitment, buy small and layer up, and remember you cannot cancel.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;As described above &lt;strong&gt;Compute Savings Plan&lt;/strong&gt; represents a flexible savings plan that supports multiple regions, and services and you commit to a dollar amount instead of an instance type or size. But there are dangers as if you calculate wrong then you are stuck paying that committed dollar amount.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you purchase Savings Plans you commit to a certain dollar amount on a period and you can not cancel. Therefore be careful and &lt;strong&gt;do your research first&lt;/strong&gt; before committing to any kind of Savings Plan.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-trim-your-aws-bill&quot;&gt;We Could Trim Your AWS Bill&lt;/h2&gt;

  &lt;p&gt;If your AWS bill is creeping up and nobody is sure which workloads to commit a Savings Plan against, this is the kind of engagement &lt;strong&gt;Clearview Team&lt;/strong&gt; takes on. We model the coverage, stage the purchase, and track utilization so you keep the discount without ever over-committing.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;/work-with-us/&quot;&gt;Scope an engagement →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="technology" />
    <category term="savings-tips" />
    <category term="cloud-computing" />
    <category term="aws" />
    <category term="internet" />
    <category term="aws-cost-optimization" />
    <category term="aws-devops" />
    
  </entry>
  
  <entry>
    <title>Migrating EC2 to AWS Graviton (ARM): A Cost-Reduction Case Study</title>
    <link href="https://blog.clearview.team/2024/embracing-the-future-of-cloud-computing/" />
    <id>https://blog.clearview.team/2024/embracing-the-future-of-cloud-computing/</id>
    <published>2024-02-11T05:59:43+01:00</published>
    <updated>2024-02-11T05:59:43+01:00</updated>
    <author>
      <name>Nedim Hadzimahmutovic</name>
    </author>
    <summary>How we cut Clearview client EC2 bills by up to 20% by migrating x86 workloads to AWS Graviton (ARM64) — the instance-family swap (M6g, C7gd, R7g), the AMI-selection step in Terraform, and the compatibility checks we ran first.</summary>
    <content type="html">&lt;p&gt;&lt;img src=&quot;/assets/images/posts/embracing-the-future-of-cloud-computing/cover.svg&quot; alt=&quot;Editorial cover — an ARM Graviton processor die with circuit traces, brand-coloured glow, set on a dark Ink backdrop.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The world of cloud computing is changing fast, that is a fact. Some changes have a more significant impact, as was the case with the rise of AWS Graviton processors.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The whole tech industry is experiencing the growing adoption of &lt;strong&gt;ARM&lt;/strong&gt; architecture, so why should it not be the case with the Cloud as well?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This article focuses on the impact of AWS Graviton on the Cloud offering, the shift towards the &lt;strong&gt;ARM&lt;/strong&gt; architecture, and how &lt;strong&gt;Clearview&lt;/strong&gt; is helping clients jump on the Graviton train and &lt;strong&gt;save on costs up to 20%&lt;/strong&gt;.&lt;/p&gt;

&lt;h3 id=&quot;key-points-on-aws-graviton&quot;&gt;Key points on AWS Graviton&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;AWS Graviton processors are becoming popular because of their &lt;strong&gt;superior performance&lt;/strong&gt; and &lt;strong&gt;energy efficiency&lt;/strong&gt; in cloud environments which reduces your carbon footprint,&lt;/li&gt;
  &lt;li&gt;Adapting the ARM architecture &lt;strong&gt;requires data migration&lt;/strong&gt; or &lt;strong&gt;possible code improvements&lt;/strong&gt; before benefiting from the superior improvements in both cost and performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;the-rise-of-aws-graviton&quot;&gt;The Rise of AWS Graviton&lt;/h3&gt;

&lt;h4 id=&quot;understanding-aws-graviton-processors&quot;&gt;Understanding AWS Graviton Processors&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/embracing-the-future-of-cloud-computing/processors.svg&quot; alt=&quot;An ARM Graviton die at the centre with three benefit callouts radiating outward: performance, cost-effectiveness, energy efficiency.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;AWS Graviton processors are powered by &lt;strong&gt;ARM&lt;/strong&gt; architecture, enabling performance and cost optimizations, which represent a significant advancement in cloud computing, compared to traditional x86 processors.&lt;/p&gt;

&lt;p&gt;Graviton processors are optimized for Cloud workloads with the following benefits:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Large L1 and L2 &lt;strong&gt;caches for every virtual central processing unit&lt;/strong&gt; (vCPU), meaning that most of your workload never burdens RAM,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Every virtual CPU is a physical core&lt;/strong&gt;, meaning more isolation between virtual CPUs,&lt;/li&gt;
  &lt;li&gt;Cores are &lt;strong&gt;connected in a fast mesh&lt;/strong&gt; with ~2TB/s of bisection bandwidth, which allows applications to move very quickly from core to core,&lt;/li&gt;
  &lt;li&gt;Graviton’s RAM architecture means &lt;strong&gt;you don’t need to worry about application memory allocation&lt;/strong&gt;, or &lt;strong&gt;which cores are running the application&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;generations-of-aws-graviton&quot;&gt;Generations of AWS Graviton&lt;/h3&gt;

&lt;p&gt;Graviton processors come in &lt;strong&gt;various generations&lt;/strong&gt; with each generation offering various advances in processing power and efficiency.&lt;/p&gt;

&lt;h4 id=&quot;graviton&quot;&gt;Graviton&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;first generation&lt;/strong&gt; ARM architecture-based, known as A1 type, Graviton-powered EC2 instances were launched in 2018. Not every region is still providing this first-generation instance type. Use the following awscli command to check if a region supports the A1 instance type.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-instance-type-offerings &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--location-type&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;availability-zone&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--filters&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;location,Values&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;us-east-2a &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-2 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--query&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;InstanceTypeOfferings[*].[InstanceType]&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt; text &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt; default | &lt;span class=&quot;nb&quot;&gt;sort&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;a1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This processor features:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;64-bit ARM Neoverse cores,&lt;/li&gt;
  &lt;li&gt;The instances are up to 40% less expensive than the same number of vCPUs and DRAM available in other instance types.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;graviton-2&quot;&gt;Graviton 2&lt;/h4&gt;

&lt;p&gt;Launched in 2019 it represents the &lt;strong&gt;second generation&lt;/strong&gt; of AWS Graviton processes. Graviton2-based instance types offer up to 40% better price performance than fifth-generation instances.&lt;/p&gt;

&lt;h4 id=&quot;graviton-2-instance-types&quot;&gt;Graviton 2 Instance types&lt;/h4&gt;

&lt;p&gt;We now have 12 instance families (M6g, M6gd, C6g, C6gd, C6gn, R6g, R6gd, T4g, X2gd, Im4gn, Is4gen, and G5g) that are powered by AWS Graviton2 processors that provide significant price performance benefits for a wide range of workloads.&lt;/p&gt;

&lt;h4 id=&quot;graviton-3&quot;&gt;Graviton 3&lt;/h4&gt;

&lt;p&gt;Launched in 2022, it is the &lt;strong&gt;third and latest generation&lt;/strong&gt; that is generally available. When compared to AWS Graviton2 processors it provides:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;up to &lt;strong&gt;25% better computing&lt;/strong&gt; performance,&lt;/li&gt;
  &lt;li&gt;up to 2x better floating-point performance,&lt;/li&gt;
  &lt;li&gt;up to 2x faster crypto performance, and&lt;/li&gt;
  &lt;li&gt;up to 3x better ML performance,&lt;/li&gt;
  &lt;li&gt;support for bfloat16,&lt;/li&gt;
  &lt;li&gt;and features the latest DDR5 memory, which provides 50% more memory bandwidth compared to DDR4.&lt;/li&gt;
  &lt;li&gt;up to &lt;strong&gt;60% less energy usage&lt;/strong&gt; for the same performance than comparable EC2 instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;graviton-3-instance-types&quot;&gt;Graviton 3 instance types&lt;/h4&gt;

&lt;p&gt;All 7th generation instance types containing “g” means it is based on AWS Graviton 3, the silicon designed by AWS. Examples:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;C7g, C7gd, C7gn:&lt;/strong&gt; the “C” instance family is designed for compute-intensive workloads,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;M7g&lt;/strong&gt;, &lt;strong&gt;M7gd&lt;/strong&gt;: “G” instance family is designed for general-purpose workloads with balanced computing, memory, and networking,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;R7g, R7gd&lt;/strong&gt;: The “R” instance family is designed for memory-intensive workloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;graviton-4&quot;&gt;Graviton 4&lt;/h4&gt;

&lt;p&gt;Launched in late November 2023 Graviton 4 represents the &lt;strong&gt;fourth generation&lt;/strong&gt; and it is &lt;strong&gt;still in preview only&lt;/strong&gt;, with general availability planned in Q1 of 2024. When compared to Graviton3 processors it provides:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;up to &lt;strong&gt;30% better&lt;/strong&gt; computing performance,&lt;/li&gt;
  &lt;li&gt;50% more cores,&lt;/li&gt;
  &lt;li&gt;75% more memory bandwidth.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Graviton4 will be available in memory-optimized Amazon EC2 R8g instances, which are currently in preview only. R8g instances offer larger instance sizes with up to 3x more vCPUs and 3x more memory than current generation R7g instances. To learn more about Graviton4-based R8g instances, visit this &lt;a href=&quot;https://aws.amazon.com/ec2/instance-types/r8g/&quot;&gt;link&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;switch-your-aws-services-to-graviton&quot;&gt;Switch your AWS Services to Graviton&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/embracing-the-future-of-cloud-computing/migration.svg&quot; alt=&quot;Three migration rows from x86 to Graviton — EC2 m5 → m7g, RDS db.m5 → db.m7g, ElastiCache cache.m5 → cache.m7g.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Here we will describe which services we have switched over to AWS Graviton.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Some services can be switched with a few clicks, while others require a full data migration following best practices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;aws-ec2&quot;&gt;AWS EC2&lt;/h4&gt;

&lt;p&gt;Following is a tutorial on how we switched &lt;strong&gt;EC2&lt;/strong&gt; instances. Please note that t4g is run on &lt;strong&gt;Graviton 2&lt;/strong&gt;. Migrating our NodeJScode to be compatible with &lt;strong&gt;ARM&lt;/strong&gt; architecture meant upgrading a few packages only.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note: do not forget to set your awscli profile as in all of these examples the default one is used.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;find-arm-lts-ubuntu-images&quot;&gt;Find ARM LTS Ubuntu Images&lt;/h4&gt;

&lt;p&gt;For the latest LTS which is 22.04 LTS (Jammy Jellyfish), currently.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-images &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--filters&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server-*&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Name=architecture,Values=arm64&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--query&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;sort_by(Images, &amp;amp;CreationDate)[].[Name, ImageId, Architecture]&quot;&lt;/span&gt;  &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt; text &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This command will show you the IDs of the images that support the ARM architecture. Choose one and set it at the next step.&lt;/p&gt;

&lt;h4 id=&quot;set-ami-id&quot;&gt;Set AMI ID&lt;/h4&gt;

&lt;p&gt;As the AMI ID has been found in the previous command, we need to save it to a variable.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AMI_ID=ami-02ddaf75821f25213
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;list-ec2-key-pairs&quot;&gt;List EC2 Key Pairs&lt;/h4&gt;

&lt;p&gt;The following commands will show keys that were added previously. If you get an empty result you need to add your public SSH key.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-key-pairs &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;find-the-availability-zone&quot;&gt;Find the Availability Zone&lt;/h4&gt;

&lt;p&gt;The zone is directly connected to the subnet.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-instance-type-offerings &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;--location-type&lt;/span&gt; availability-zone &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--filters&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;Name&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;instance-type,Values&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;t4g.small &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note: This step is very important as you need to find the what availability zone supports your instance size.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;find-the-subnet-and-security-group&quot;&gt;Find the Subnet and Security Group&lt;/h4&gt;

&lt;p&gt;We need to find the correct Subnet and SG.&lt;/p&gt;

&lt;h4 id=&quot;list-security-groups&quot;&gt;List Security Groups&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-security-groups &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--query&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;SecurityGroups[*].{Name:GroupName,ID:GroupId}&quot;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;list-subnets&quot;&gt;List subnets&lt;/h4&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 describe-subnets &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note: Find the correct subnet that is used by your old EC2 instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;create-the-ec2-instance&quot;&gt;Create the EC2 Instance&lt;/h4&gt;

&lt;p&gt;Please note that the following command is an example only and that you need to change the values for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;the key name,&lt;/li&gt;
  &lt;li&gt;the subnet ID,&lt;/li&gt;
  &lt;li&gt;the security group ID,&lt;/li&gt;
  &lt;li&gt;set your tags,&lt;/li&gt;
  &lt;li&gt;use your profile name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt; &lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;aws ec2 run-instances &lt;span class=&quot;nt&quot;&gt;--image-id&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$AMI_ID&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--instance-type&lt;/span&gt; t4g.small  &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--key-name&lt;/span&gt; nedim &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--subnet-id&lt;/span&gt; subnet-000000000000000 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--security-group-ids&lt;/span&gt; sg-00000000000000000 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--associate-public-ip-address&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--tag-specifications&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;ResourceType=instance,Tags=[{Key=Name,Value=ubuntu-test-graviton}]&apos;&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--region&lt;/span&gt; us-east-1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;--profile&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;default
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;prepare-the-image&quot;&gt;Prepare the image&lt;/h4&gt;

&lt;p&gt;Connect to the VM, and run the system upgrade.&lt;/p&gt;

&lt;h4 id=&quot;new-server-configuration-with-ansible&quot;&gt;New Server Configuration with Ansible&lt;/h4&gt;

&lt;p&gt;Since we used Ansible to set up the old server the process of setting up an identically configured server was a piece of cake. All we had to do was change the hostname in the host inventory file add the new server and rerun the playbooks.&lt;/p&gt;

&lt;h4 id=&quot;volume-size&quot;&gt;Volume size&lt;/h4&gt;

&lt;p&gt;Make sure the new disk volume is identical to the size of the old server’s disk volume as we will copy over data from the old server to the new one.&lt;/p&gt;

&lt;h4 id=&quot;data-migration&quot;&gt;Data Migration&lt;/h4&gt;

&lt;p&gt;Using RSYNC, copy over the data from the old server to the new Graviton-based one, using the SSH protocol.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rsync -avz -e ssh /var/www/ root@enew-server.compute-1.amazonaws.com:/var/www/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;Note: This data migration does not guarantee that the code will work 100%, you will need to deploy and build the code again to be sure it works as expected. In our case, we needed to upgrade several NodeJS packages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;aws-rds&quot;&gt;AWS RDS&lt;/h3&gt;

&lt;p&gt;With RDS instances no migration is necessary and you can take the easy way of just modifying the instance class of your instances if your database engine version supports it.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Determine if the current database version meets the minimum required version for moving to Graviton2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Version supporting Graviton:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;MySQL&lt;/strong&gt;: 8.0.17 and higher,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt;: 12.3, 13 and higher,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;MariaDB&lt;/strong&gt;: 10.4.13, 10.5 and higher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your RDS instance isn’t at a version supported by Graviton2, you need to upgrade to a supported version.&lt;/p&gt;

&lt;h3 id=&quot;aws-elasticache-for-redis&quot;&gt;AWS ElastiCache for Redis&lt;/h3&gt;

&lt;p&gt;Migrating Amazon Elasticache for Redis and Memcached to Graviton2-based instances offers a &lt;strong&gt;smooth transition&lt;/strong&gt;, only a change of instance type is required, with a performance improvement of up to 45%, all at a 20% lower cost compared to similar x86-based instances.&lt;/p&gt;

&lt;h3 id=&quot;the-beginning-of-a-new-age-in-cloud-computing&quot;&gt;The Beginning of a New Age in Cloud Computing&lt;/h3&gt;

&lt;p&gt;In conclusion, the rise of &lt;strong&gt;AWS Graviton&lt;/strong&gt; and &lt;strong&gt;ARM&lt;/strong&gt; architecture represents a change in cloud computing that impacts the whole IT industry. These technologies bring together &lt;strong&gt;improved performance&lt;/strong&gt;, &lt;strong&gt;cost-effectiveness&lt;/strong&gt;, and &lt;strong&gt;energy efficiency&lt;/strong&gt;. As more organizations adopt these technologies, we can expect a future where the flexibility of &lt;strong&gt;ARM&lt;/strong&gt; &lt;strong&gt;architecture&lt;/strong&gt; and the efficiency of &lt;strong&gt;Graviton&lt;/strong&gt; processors set the &lt;strong&gt;standard for cloud infrastructure&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Therefore, do not miss the chance to make the switch and be part of the new standard.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-migrate-you&quot;&gt;We Could Migrate You&lt;/h2&gt;

  &lt;p&gt;If you are running EC2 workloads on x86 and the bill is starting to bite, this is the kind of engagement &lt;strong&gt;Clearview Team&lt;/strong&gt; takes on. We benchmark your workload on Graviton, plan the migration around your release cadence, and ship the Terraform that keeps you on ARM.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;/work-with-us/&quot;&gt;Scope an engagement →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="aws-cost-optimization" />
    <category term="cloud-computing" />
    <category term="arm" />
    <category term="aws-graviton" />
    <category term="aws" />
    <category term="aws-devops" />
    
  </entry>
  
  <entry>
    <title>Deploying Node Containers on AWS App Runner with Terraform</title>
    <link href="https://blog.clearview.team/2024/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/" />
    <id>https://blog.clearview.team/2024/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/</id>
    <published>2024-01-11T07:01:42+01:00</published>
    <updated>2026-08-11T00:00:00+02:00</updated>
    <author>
      <name>Nedim Hadzimahmutovic</name>
    </author>
    <summary>Why AWS App Runner is our default for container workloads — the `aws_apprunner_service` Terraform resource, ECR wiring, and the auto-scaling defaults we actually keep in production.</summary>
    <content type="html">&lt;h3 id=&quot;the-smart-way-to-deploy-containers-on-the-cloud&quot;&gt;The Smart Way to Deploy Containers on The Cloud&lt;/h3&gt;

&lt;h4 id=&quot;say-hello-to-aws-apprunner&quot;&gt;Say hello to AWS AppRunner&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/cover.svg&quot; alt=&quot;Editorial cover — three container blocks (api, web, worker) resolving into a single AppRunner https endpoint, brand colours throughout.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Containers on AWS AppRunner&lt;/p&gt;

&lt;aside class=&quot;callout callout--warning&quot;&gt;
  &lt;p&gt;&lt;strong&gt;Heads up — AppRunner is sunsetting.&lt;/strong&gt; As of &lt;strong&gt;April 30, 2026&lt;/strong&gt;, AWS App Runner no longer accepts new customers. Existing App Runner services keep running and AWS continues to invest in security and availability, but no new features are planned. For new container workloads, AWS recommends &lt;strong&gt;Amazon ECS Express Mode&lt;/strong&gt; — a new capability inside ECS designed to give you the same one-command deploy without the App Runner lock-in. This post stays up as a reference for the Terraform shape and the deployment thinking; if you are starting a new container workload today, reach for ECS Express Mode instead.&lt;/p&gt;
&lt;/aside&gt;

&lt;h3 id=&quot;why-apprunner&quot;&gt;Why AppRunner?&lt;/h3&gt;

&lt;h4 id=&quot;it-is-easy-to-deploy-and-manage&quot;&gt;It is easy to deploy and manage&lt;/h4&gt;

&lt;p&gt;If you tried to deploy containers on the cloud you have experienced anything but an easy process. It has been easy to deploy containers on your local machine but deploying them on the Cloud has not been an easy process.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;That was true until AWS AppRunner was introduced back in 2021.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;it-is-a-great-solution-for-startups&quot;&gt;It is a great solution for startups&lt;/h4&gt;

&lt;p&gt;Here at &lt;a href=&quot;https://clearview.team/&quot;&gt;&lt;strong&gt;Clearview&lt;/strong&gt;&lt;/a&gt;, we have found that AppRunner can be of very benefit to startup projects as it is cost-effective because of its on-demand pricing model and it is a fully managed service.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;This will result in less time spent on setting up the infrastructure which saves us time and money.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;it-has-an-attractive-pricing-model&quot;&gt;It has an attractive pricing model&lt;/h4&gt;

&lt;p&gt;What is great about AppRunner is that when the container is in an &lt;strong&gt;idle state&lt;/strong&gt; you only pay for the memory provisioned in each container instance. When a container is in the idle state it is considered to be a &lt;strong&gt;Provisioned container instance&lt;/strong&gt;. Once the container starts to use CPU resources, it will be considered an &lt;strong&gt;Active container instance&lt;/strong&gt; and you will be charged for the CPU resources used.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;As you can pause and resume the service you can as well additionally save on costs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;in-summary&quot;&gt;In summary&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Provisioned container instances&lt;/strong&gt; — charged for memory
($0.007 / GB-hour),&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Active container instances&lt;/strong&gt; — charged for both memory and CPU resources used
($0.064 / vCPU-hour, $0.007 / GB-hour),&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Paused instances&lt;/strong&gt;— CPU and memory are not charged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;what-can-apprunner-do&quot;&gt;What can AppRunner do?&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/features.svg&quot; alt=&quot;Four AppRunner features as cards: auto-scaling, builds from ECR or source, managed TLS, and integrated logs and metrics.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Since AWS App Runner is a fully managed service it will&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;automatically deploy&lt;/strong&gt; web applications by detecting a new container image that has been pushed to the container registry,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;automatically scale and load balance&lt;/strong&gt; to meet the traffic needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Automatic deployment, scaling, and load balancing make it a desirable technology.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;easily-managed-by-terraform&quot;&gt;Easily Managed by Terraform&lt;/h4&gt;

&lt;p&gt;By adding Terraform to the mix we can provision this technology even faster. Later in this article, we will provide examples of code on how to set up AppRunner with Terraform.&lt;/p&gt;

&lt;h3 id=&quot;history&quot;&gt;History&lt;/h3&gt;

&lt;p&gt;Back when it was introduced it was still an early tech and still not developed and mature but in the last two years, it has grown significantly. Here are some improvements that we have witnessed while using the tech:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Added &lt;strong&gt;support for more regions&lt;/strong&gt; across the world,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reduced duration for deploying&lt;/strong&gt; applications using container images, which is about a 30–40% reduction in deployment time depending on the container image size,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Increased instance startup time&lt;/strong&gt; from one to a maximum of five minutes which enables slower instances that need more time of startup to be used,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Added dual stack&lt;/strong&gt; support for incoming traffic through public endpoints, both IPv4 and IPv6 endpoints, simultaneously,&lt;/li&gt;
  &lt;li&gt;Reduced the time taken for image-based service deployment,
Added support for immediate deployment failure if App Runner couldn’t pull an image.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;provisioning-apprunner-with-terraform&quot;&gt;Provisioning AppRunner with Terraform&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/terraform.svg&quot; alt=&quot;Terraform plan on the left listing aws_ecr_repository, aws_iam_role, aws_apprunner_service and a custom domain; on the right a running AppRunner service with a green check next to its https endpoint.&quot; /&gt;&lt;/p&gt;

&lt;p&gt;In this demonstration, we will provision AWS services with Terraform as follows:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;ECR for hosting container images,&lt;/li&gt;
  &lt;li&gt;NodeJS API service on AppRunner,&lt;/li&gt;
  &lt;li&gt;NuxtJS FrontEnd service on AppRunner.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Local variables are defined at the top of the .tf file to be straightforward about what values should be changed depending on the project.&lt;/p&gt;

&lt;h4 id=&quot;diagram&quot;&gt;Diagram&lt;/h4&gt;

&lt;p&gt;In the diagram, it is clear that the publicly exposed service is the FrontEnd service, which then connects to the API service. To point and connect one service to the other environment variables are used.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/the-easy-and-smart-way-to-deploy-containers-on-the-cloud-say-hello-to-apprunner/1_jbSglooJmw6b7lRUtXWipQ.png&quot; alt=&quot;AWS AppRunner architecture diagram&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;provision-ecr&quot;&gt;Provision ECR&lt;/h4&gt;

&lt;p&gt;This is an example of how to provision the ECR for API.&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_apprunner_service_with_terraform/blob/e4c96bc5e856753515c2c640e853f0deeef5e71f/ecr.tf#L1-L36&quot;&gt;aws_apprunner_service_with_terraform/ecr.tf at e4c96bc5e856753515c2c640e853f0deeef5e71f · clearview/aws_apprunner_service_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# my_api&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_ecr_repository&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_ecr_api&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;                 &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_ecr/my_api&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;image_tag_mutability&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MUTABLE&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;image_scanning_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;scan_on_push&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_ecr_lifecycle_policy&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_ecr_api&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;repository&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_ecr_repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_ecr_api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;policy&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;jsonencode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;rules&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;rulePriority&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;keep last 10 images&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;action&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;expire&quot;&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;selection&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;tagStatus&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;any&quot;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;countType&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;imageCountMoreThan&quot;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;countNumber&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_ecr_api_registry_id&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_ecr_repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_ecr_api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;registry_id&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_ecr_api_repository_url&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_ecr_repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_ecr_api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;repository_url&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;provision-of-the-api-service&quot;&gt;Provision of the API Service&lt;/h4&gt;

&lt;p&gt;In this example, the health check is performed against a custom endpoint available on the/api/healtcheck URN, but that might be different in your case.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;To be safe and if you do not have a health check endpoint yet just use / .&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This instance is provisioned with the following configuration:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;1024 CPU units,&lt;/li&gt;
  &lt;li&gt;2048 MB of memory,&lt;/li&gt;
  &lt;li&gt;The health check endpoint is located at /api/healthcheck ,&lt;/li&gt;
  &lt;li&gt;Container port running on 3000 .&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_apprunner_service_with_terraform/blob/e4c96bc5e856753515c2c640e853f0deeef5e71f/api_service.tf#L1-L75&quot;&gt;aws_apprunner_service_with_terraform/api_service.tf at e4c96bc5e856753515c2c640e853f0deeef5e71f · clearview/aws_apprunner_service_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;locals&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;api_port&lt;/span&gt;                   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;api_domain&lt;/span&gt;                 &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;api.mydomain.team&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;api_git_repo&lt;/span&gt;               &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my/my_api&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;api_development_git_branch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dev&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;api_ecr_image_development&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;${aws_ecr_repository.my_api_ecr.repository_url}:dev&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_apprunner_service&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_api&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;depends_on&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;aws_ecr_repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_api_ecr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;aws_iam_role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_app_runner_roles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;aws_apprunner_vpc_connector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_vpc_connector&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_api&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;source_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;authentication_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;access_role_arn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_iam_role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_app_runner_roles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arn&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;image_repository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_identifier&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;api_ecr_image_development&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_repository_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ECR&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;port&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3000&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;runtime_environment_variables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# Server Port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;PORT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;3000&quot;&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;#&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NODE_ENV&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;development&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;LOG_LEVEL&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;PORT&lt;/span&gt;           &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;api_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;CONTAINER_PORT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;api_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;HOST_PORT&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;api_port&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;runtime_environment_secrets&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;auto_deployments_enabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;health_check_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;                &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/api/healthcheck&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;healthy_threshold&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;protocol&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;HTTP&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;timeout&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;unhealthy_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;
  &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;instance_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;cpu&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2048&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;memory&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;4096&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;network_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;egress_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;egress_type&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;VPC&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;vpc_connector_arn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_apprunner_vpc_connector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_vpc_connector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arn&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my-my_api-apprunner-service&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_api_apprunner_my_api_service_url&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_apprunner_service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_api&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;service_url&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;provision-of-the-frontend-service&quot;&gt;Provision of the FrontEnd service&lt;/h4&gt;

&lt;p&gt;This instance is provisioned with the following configuration:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;1024 CPU units,&lt;/li&gt;
  &lt;li&gt;2048 MB of memory,&lt;/li&gt;
  &lt;li&gt;The health check endpoint is located at /ping .&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_apprunner_service_with_terraform/blob/e4c96bc5e856753515c2c640e853f0deeef5e71f/fe_service.tf#L1-L91&quot;&gt;aws_apprunner_service_with_terraform/fe_service.tf at e4c96bc5e856753515c2c640e853f0deeef5e71f · clearview/aws_apprunner_service_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;locals&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;                   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3001&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_domain&lt;/span&gt;                 &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;frontend.my-domain.org&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_apprunner_domain&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;members.development.my-domain.org&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_ecr_image_development&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;${aws_ecr_repository.my_fe_ecr.repository_url}:dev&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_git_repo&lt;/span&gt;               &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;clearview/my-members-fe&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;my_fe_development_git_branch&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;dev&quot;&lt;/span&gt;
&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_apprunner_service&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_fe&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;depends_on&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;aws_ecr_repository&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_ecr&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;service_name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_fe&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;source_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;authentication_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;access_role_arn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_iam_role&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_app_runner_roles&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arn&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;image_repository&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_identifier&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_ecr_image_development&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_repository_type&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ECR&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;image_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;port&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;start_command&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;yarn dev&quot;&lt;/span&gt;
        &lt;span class=&quot;nx&quot;&gt;runtime_environment_variables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;HOST&lt;/span&gt;           &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;HOSTNAME&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NITRO_HOST&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;ENV&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;development&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NODE_ENV&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;development&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;PORT&lt;/span&gt;           &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NITRO_PORT&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;CONTAINER_PORT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;HOST_PORT&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# API&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;PROXY_TARGET&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://${local.api_domain}&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_API_BASE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;https://${local.api_domain}&quot;&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# Nuxt&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_PUBLIC_DEV&lt;/span&gt;      &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_PUBLIC_DEBUG&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_PUBLIC_SITE_URL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;http://localhost:${local.my_fe_port}&quot;&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_PORT&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;local&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe_port&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;NUXT_HOST&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;0.0.0.0&quot;&lt;/span&gt;
          &lt;span class=&quot;c1&quot;&gt;# Debug&lt;/span&gt;
          &lt;span class=&quot;nx&quot;&gt;LOG_LEVEL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;debug&quot;&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;auto_deployments_enabled&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;health_check_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;path&lt;/span&gt;                &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;/ping&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;healthy_threshold&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;interval&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;protocol&lt;/span&gt;            &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;HTTP&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;timeout&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;19&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;unhealthy_threshold&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;
  &lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;instance_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;cpu&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;1024&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;memory&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;2048&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;network_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;egress_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;egress_type&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;VPC&quot;&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;vpc_connector_arn&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_apprunner_vpc_connector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_vpc_connector&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;arn&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;nx&quot;&gt;ingress_configuration&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;nx&quot;&gt;is_publicly_accessible&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;tags&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;Name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my-my_fe-apprunner-service&quot;&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;nx&quot;&gt;output&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;my_fe_apprunner_my_fe_service_url&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;value&lt;/span&gt;       &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;aws_apprunner_service&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;my_fe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;service_url&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;description&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&quot;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;

&lt;p&gt;In this article, we covered AppRunner from its humble beginnings to the powerful technology it has proven to be today. We provided useful diagrams and Terraform code for easy provisioning of service.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;If you have any questions please do not hesitate to contact our team.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-move-you-off-apprunner-cleanly&quot;&gt;We Could Move You Off AppRunner Cleanly&lt;/h2&gt;

  &lt;p&gt;If you have App Runner in production today and the April 2026 sunset notice has you wondering what to do, that is the engagement &lt;strong&gt;Clearview Team&lt;/strong&gt; has in front of us right now. The target is ECS Express Mode, the Terraform stays small, and we cut over in stages so your traffic does not notice. We have done the same shape of work moving teams off Fargate behind a bespoke ALB and off self-managed Kubernetes — the muscle memory carries.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;mailto:info@clearview.team?subject=AppRunner%20to%20ECS%20Express%20Mode%20migration&quot;&gt;Brief us on your AppRunner stack →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="aws-app-runner" />
    <category term="cloud" />
    <category term="containers" />
    <category term="nodejs" />
    <category term="nuxtjs" />
    <category term="aws-devops" />
    
  </entry>
  
  <entry>
    <title>AWS Budgets and Alerts with Terraform: A Cost-Monitoring Case Study</title>
    <link href="https://blog.clearview.team/2024/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/" />
    <id>https://blog.clearview.team/2024/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/</id>
    <published>2024-01-03T13:47:03+01:00</published>
    <updated>2026-08-11T00:00:00+02:00</updated>
    <author>
      <name>Nedim Hadzimahmutovic</name>
    </author>
    <summary>How we set AWS budgets and Slack alerts with Terraform — the `aws_budgets_budget` notification block, its `subscriber_email_addresses`, and how to route a real breach into SNS before the invoice hits the CFO&apos;s inbox.</summary>
    <content type="html">&lt;h3 id=&quot;monitor-and-control-amazon-cloud-costs-with-terraform&quot;&gt;Monitor and Control Amazon Cloud Costs with Terraform&lt;/h3&gt;

&lt;h4 id=&quot;for-every-cloud-project-you-need-to-set-budgets-and-alerts-with-terraform&quot;&gt;For every Cloud project, you need to set budgets and alerts with Terraform&lt;/h4&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/cover.svg&quot; width=&quot;1600&quot; height=&quot;900&quot; alt=&quot;AWS Budgets with Terraform — descending cost line with budget cap and alert dot&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Budgets with Terraform&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;why-set-up-a-budget-for-your-cloud-account&quot;&gt;Why set up a budget for your cloud account?&lt;/h3&gt;

&lt;p&gt;If you wonder why most startups fail it is simple, they run out of money and there have been many horror stories about crazy big cloud bills.&lt;/p&gt;

&lt;p&gt;At &lt;a href=&quot;https://clearview.team/&quot;&gt;&lt;strong&gt;&lt;em&gt;Clearview&lt;/em&gt;&lt;/strong&gt;&lt;/a&gt;, when we put a new client on the Cloud, we know they will get a bill eventually. It’s our responsibility to make sure that the bill will not be a big and sudden surprise.&lt;/p&gt;

&lt;h3 id=&quot;setting-up-the-budgets&quot;&gt;Setting up the budgets&lt;/h3&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/setting-up.svg&quot; width=&quot;1200&quot; height=&quot;420&quot; alt=&quot;Four required pieces of an AWS budget — name, limit amount, period, alert threshold&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;Setting the AWS Cloud budget&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The first step is to set budgets and alerts. We usually set:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;The Account wide budget,&lt;/li&gt;
  &lt;li&gt;The Most used service budget,&lt;/li&gt;
  &lt;li&gt;The Most important tag budget.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every budget sends out an email notification when the budget cost threshold is reached, in these two cases:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The ACTUAL cost&lt;/strong&gt;
which will be triggered once that amount has already been spent,&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The FORECASTED cost&lt;/strong&gt;
notification will be triggered earlier as the cost is based on prediction based on your past usage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As we are a tech company the budgets are set using &lt;strong&gt;Terraform&lt;/strong&gt; and in the next section, you can find diagrams and code samples that explain how budgets and notifications work.&lt;/p&gt;

&lt;h3 id=&quot;monthly-account-budget&quot;&gt;Monthly Account Budget&lt;/h3&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/monthly-cap.svg&quot; width=&quot;1200&quot; height=&quot;420&quot; alt=&quot;Monthly account budget with three alert thresholds at 50, 80, and 100 percent&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;Set your Monthly AWS Budget.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This budget tracks account-wide costs.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/1_DjYvXzclbSKPmda0FHPUWQ.png&quot; alt=&quot;AWS budget configuration diagram&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_budgets_budget&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;monthly_account_budget&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Monthly Budget for my Account&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;budget_type&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COST&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;500&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_unit&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;USD&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;time_unit&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MONTHLY&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ACTUAL&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;FORECASTED&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform/blob/0b8af27933b32c27c7b09c4b633724ed17fe82b9/monthly_account_budget.tf#L1-L23&quot;&gt;aws_budgets_notifications_with_terraform/monthly_account_budget.tf at 0b8af27933b32c27c7b09c4b633724ed17fe82b9 · clearview/aws_budgets_notifications_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;the-notification-block-arguments&quot;&gt;The notification block arguments&lt;/h4&gt;

&lt;p&gt;Each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notification&lt;/code&gt; block inside &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;aws_budgets_budget&lt;/code&gt; controls one
alert. The five arguments the AWS provider expects are:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;comparison_operator&lt;/code&gt;&lt;/strong&gt; — one of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GREATER_THAN&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;LESS_THAN&lt;/code&gt;, or
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EQUAL_TO&lt;/code&gt;. For a spend budget you almost always use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GREATER_THAN&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;threshold&lt;/code&gt;&lt;/strong&gt; — a number. Compared against the budgeted amount.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;threshold_type&lt;/code&gt;&lt;/strong&gt; — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PERCENTAGE&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ABSOLUTE_VALUE&lt;/code&gt;. Percentage
is easier to reason about because you don&apos;t have to rewrite the
block when the budget limit changes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notification_type&lt;/code&gt;&lt;/strong&gt; — &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ACTUAL&lt;/code&gt; (the current spend) or
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FORECASTED&lt;/code&gt; (AWS&apos;s prediction based on past usage). Set one of
each per budget so you get both an early warning and a real breach
alert.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscriber_email_addresses&lt;/code&gt;&lt;/strong&gt; — a &lt;strong&gt;list of email strings&lt;/strong&gt;.
This is where the alert lands. Every address in the list receives
a copy. Note: it is a Terraform list, not a comma-separated
string, so use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[&quot;a@x&quot;, &quot;b@x&quot;]&lt;/code&gt; shape not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&quot;a@x, b@x&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are two adjacent arguments that this example does not use but
you might reach for:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscriber_sns_topic_arns&lt;/code&gt;&lt;/strong&gt; — send the notification to an SNS
topic instead of (or in addition to) email. Useful when you want
the alert to fan out into Slack via an SNS→Lambda hop.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscriber_type&lt;/code&gt;&lt;/strong&gt; on the older &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;notification&lt;/code&gt; shape has been
replaced by the three &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;subscriber_*&lt;/code&gt; list arguments; you should
not need it on modern provider versions (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hashicorp/aws &amp;gt;= 4.0&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full provider docs: &lt;a href=&quot;https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/budgets_budget#notification&quot;&gt;aws_budgets_budget#notification&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id=&quot;budgets-by-service&quot;&gt;Budgets by Service&lt;/h3&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/by-service.svg&quot; width=&quot;1200&quot; height=&quot;420&quot; alt=&quot;Budgets scoped per AWS service — EC2, S3, RDS, AppRunner&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS Budgets illustration.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h4 id=&quot;ec2-monthly-budget&quot;&gt;EC2 Monthly Budget&lt;/h4&gt;

&lt;p&gt;This budget will track costs for EC2 services only.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/1_nB9REBXD-JJqMweyPN5ReQ.png&quot; alt=&quot;AWS budget configuration diagram&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_budgets_budget&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ec2_monthly_budget&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;My EC2 Monthly Budget&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;budget_type&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COST&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;400&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_unit&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;USD&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;time_unit&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MONTHLY&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;cost_filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Service&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;Amazon Elastic Compute Cloud - Compute&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ACTUAL&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;100&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;FORECASTED&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform/blob/0b8af27933b32c27c7b09c4b633724ed17fe82b9/ec2_monthly_account_budget.tf#L1-L30&quot;&gt;aws_budgets_notifications_with_terraform/ec2_monthly_account_budget.tf at 0b8af27933b32c27c7b09c4b633724ed17fe82b9 · clearview/aws_budgets_notifications_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;s3-monthly-budget&quot;&gt;S3 Monthly Budget&lt;/h4&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/s3-budget.svg&quot; width=&quot;1200&quot; height=&quot;380&quot; alt=&quot;S3 monthly budget with usage bar and 75 percent alert marker&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;AWS S3 Monthly Budget.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;This budget will track costs for S3 buckets only.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/1_6k0O3vCONWjcWbY-5mLdIw.png&quot; alt=&quot;AWS budget configuration diagram&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_budgets_budget&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;s3_monthly_budget&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;My S3 Monthly Budget&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;budget_type&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COST&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;100&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_unit&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;USD&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;time_unit&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MONTHLY&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;cost_filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Service&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;Amazon Simple Storage Service&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ACTUAL&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;110&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;FORECASTED&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform/blob/0b8af27933b32c27c7b09c4b633724ed17fe82b9/s3_monthly_account_budget.tf#L1-L30&quot;&gt;aws_budgets_notifications_with_terraform/s3_monthly_account_budget.tf at 0b8af27933b32c27c7b09c4b633724ed17fe82b9 · clearview/aws_budgets_notifications_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;h4 id=&quot;apprunner-monthly-budget&quot;&gt;AppRunner Monthly Budget&lt;/h4&gt;

&lt;p&gt;This budget will track costs for AppRunner services only.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/1_bhPgTlQFCHCZI_ScAe8BMg.png&quot; alt=&quot;AWS budget configuration diagram&quot; /&gt;&lt;/p&gt;

&lt;div class=&quot;language-hcl highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;resource&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;aws_budgets_budget&quot;&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;apprunner_monthly_budget&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt;         &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;My AppRunner Monthly Budget&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;budget_type&lt;/span&gt;  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;COST&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_amount&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;100&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;limit_unit&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;USD&quot;&lt;/span&gt;
  &lt;span class=&quot;nx&quot;&gt;time_unit&lt;/span&gt;    &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MONTHLY&quot;&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;cost_filter&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;name&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Service&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;AWS App Runner&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;90&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ACTUAL&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;nx&quot;&gt;notification&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;comparison_operator&lt;/span&gt;        &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GREATER_THAN&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold&lt;/span&gt;                  &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;110&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;threshold_type&lt;/span&gt;             &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;PERCENTAGE&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;notification_type&lt;/span&gt;          &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;FORECASTED&quot;&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;subscriber_email_addresses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;email@my.team&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform/blob/0b8af27933b32c27c7b09c4b633724ed17fe82b9/apprunner_monthly_account_budget.tf#L1-L30&quot;&gt;aws_budgets_notifications_with_terraform/apprunner_monthly_account_budget.tf at 0b8af27933b32c27c7b09c4b633724ed17fe82b9 · clearview/aws_budgets_notifications_with_terraform&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;budgets-based-on-tag-filtering&quot;&gt;Budgets based on tag filtering&lt;/h3&gt;

&lt;h4 id=&quot;production-tag-monthly-budget&quot;&gt;Production Tag Monthly Budget&lt;/h4&gt;

&lt;p&gt;This budget will group multiple types of service based on the tag, in this case, the production tag is used.&lt;/p&gt;

&lt;figure class=&quot;post-figure&quot;&gt;
  &lt;img src=&quot;/assets/images/posts/before-setting-up-your-next-cloud-project-do-not-forget-to-set-the-budgets-and-alerts/1_DpawlPqyEVxauvxdWVgT8w.png&quot; width=&quot;1024&quot; height=&quot;585&quot; alt=&quot;Terraform configuration for a production-tag monthly AWS budget&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; /&gt;
  &lt;figcaption&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform/blob/0b8af27933b32c27c7b09c4b633724ed17fe82b9/production_tag_monthly_budget.tf#L1-L36&quot;&gt;production_tag_monthly_budget.tf on GitHub&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h3 id=&quot;links&quot;&gt;Links&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/clearview/aws_budgets_notifications_with_terraform&quot;&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;

&lt;p&gt;This article explained the importance of setting a cloud budget and then demonstrated how to do it programmatically. Not only that, we went a step further and created nice diagrams for each budget so that it is easier for every reader to imagine the setup.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;We hope this article finds its way and helps you avoid unnecessary cloud costs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;aside class=&quot;post-cta&quot;&gt;
  &lt;h2 id=&quot;we-could-set-this-up-for-your-team&quot;&gt;We Could Set This Up For Your Team&lt;/h2&gt;

  &lt;p&gt;If you are starting a new cloud project — or running one without budgets and alerts — this is the kind of engagement &lt;strong&gt;Clearview Team&lt;/strong&gt; takes on. We wire the budgets, the Slack alerts, and the Terraform that keeps every account on a leash, before the surprise invoice arrives.&lt;/p&gt;

  &lt;p&gt;&lt;a href=&quot;/work-with-us/&quot;&gt;Scope an engagement →&lt;/a&gt;&lt;/p&gt;
&lt;/aside&gt;
</content>
    <category term="budget" />
    <category term="cloud-computing" />
    <category term="cloud-services" />
    <category term="alerts" />
    <category term="cloud" />
    <category term="aws-cost-optimization" />
    <category term="aws-devops" />
    
  </entry>
  
</feed>
