<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>AWS McMillearn on AWS McMillearn Blog</title><link>https://aws.mcmillearn.net/</link><description>Technical blog focused on AWS best practices and architecture patterns on AWS McMillearn Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>John McMillan</managingEditor><webMaster>John McMillan</webMaster><lastBuildDate>Mon, 01 Sep 2025 10:00:00 +0000</lastBuildDate><atom:link href="https://aws.mcmillearn.net/index.xml" rel="self" type="application/rss+xml"/><category>AWS</category><category>Cloud Computing</category><category>DevOps</category><category>Architecture</category><item><title>Half Baked Images - Fully Cooked Update</title><link>https://aws.mcmillearn.net/posts/half-baked-images-update/</link><pubDate>Sun, 03 May 2026 09:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/posts/half-baked-images-update/</guid><description>AWS fixed the Distribution Settings - the Lambda workaround is no longer needed</description><content:encoded><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>Remember that Lambda function I built to work around EC2 Image Builder&rsquo;s Distribution Settings wiping your Launch Template config? You can bin it. AWS have updated the Distribution Settings behaviour and it now preserves your existing Launch Template settings when it creates a new version. The custom Lambda updater is no longer required.</p>
<h1 id="a-quick-recap">A quick recap</h1>
<p>In <a href="https://aws.mcmillearn.net/posts/half-baked-images/">Half Baked Images</a> I walked through the pain of integrating EC2 Image Builder with Terraform-managed Auto Scaling Groups. The core issue was that when EC2 Image Builder distributed a new AMI and updated your Launch Template, it created a new version containing <em>only</em> the AMI ID and account ID. Everything else, i.e. instance profile, security groups, key pair, block device mappings was gone.</p>
<p>The result? Your ASG would scale up instances with no instance profile, no security groups, and none of the configuration you&rsquo;d carefully defined. Not ideal.</p>
<p>My solution was a two-parter:</p>
<ol>
<li>Use an SSM Parameter as the source of truth for the AMI ID, updated by Image Builder&rsquo;s Distribution Settings.</li>
<li>Deploy a custom Lambda function, triggered by EventBridge, that would read the latest AMI ID from SSM and create a <em>proper</em> new Launch Template version. One that actually included all the important bits.</li>
</ol>
<p>It worked. It was reliable. But it was also plumbing that shouldn&rsquo;t have been necessary.</p>
<h1 id="what-changed">What changed?</h1>
<p>AWS have updated the EC2 Image Builder Distribution Settings behaviour. The <a href="https://docs.aws.amazon.com/imagebuilder/latest/userguide/dist-using-launch-template.html">documentation</a> now states:</p>
<blockquote>
<p>When <code>launchTemplateConfigurations</code> are present during the distribution process, Image Builder creates a new version of the launch template that includes all of the original settings from the template, and the new AMI ID from the build.</p>
</blockquote>
<p>The important bit there:  <strong>All of the original settings from the template.</strong></p>
<p>That&rsquo;s the bit that was missing before. Previously, Image Builder would create a new Launch Template version with <em>only</em> the AMI ID, effectively a blank template with a picture attached. Now it carries forward your existing configuration and simply swaps in the new AMI ID.</p>
<p>This is exactly the behaviour I expected when I first configured the <code>launch_template_configuration</code> block in my distribution config. It&rsquo;s what most people would reasonably expect. And now it actually works that way.</p>
<h1 id="what-does-this-mean-practically">What does this mean practically?</h1>
<p>If you followed my original approach, you can simplify things significantly.</p>
<h3 id="what-you-can-remove">What you can remove</h3>
<ul>
<li>The <strong>Lambda function</strong> that created new Launch Template versions with the full configuration.</li>
<li>The <strong>EventBridge rule</strong> that triggered that Lambda on successful image builds.</li>
<li>The associated <strong>IAM roles and policies</strong> for the Lambda function.</li>
<li>Any <strong>CloudWatch log groups</strong> you set up for the Lambda.</li>
</ul>
<p>That&rsquo;s a decent chunk of infrastructure that existed purely to work around a limitation that no longer exists.</p>
<h3 id="what-you-should-keep">What you should keep</h3>
<ul>
<li>The <strong>SSM Parameter</strong> approach is still a good pattern. Having a single source of truth for your AMI ID that both Terraform and Image Builder can reference is clean and avoids the bootstrapping headaches I described in the original post.</li>
<li>The <strong>lifecycle ignore</strong> on the SSM Parameter value in Terraform, this is still needed so Terraform doesn&rsquo;t fight with Image Builder over who owns that value.</li>
</ul>
<h3 id="what-to-add-back">What to add back</h3>
<p>You can now use the <code>launch_template_configuration</code> block in your distribution config with confidence:</p>
<pre tabindex="0"><code>resource &#34;aws_imagebuilder_distribution_configuration&#34; &#34;dist&#34; {
  name = &#34;${var.project}-dist&#34;

  distribution {
    region = var.region

    ami_distribution_configuration {
      name        = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      description = &#34;Custom built AL2023 image&#34;

      ami_tags = {
        OS       = &#34;AmazonLinux2023&#34;
        Hardened = &#34;true&#34;
        Name     = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      }
    }

    launch_template_configuration {
      launch_template_id = aws_launch_template.my_lt.id
      account_id         = local.account_id
      set_default_version = true
    }
  }
}
</code></pre><p>Note the <code>set_default_version = true</code>&hellip; this tells Image Builder to set the newly created version as the default. Combined with the preserved settings, this means your ASG will pick up the new AMI on the next scale event without any Lambda glue in the middle.</p>
<h1 id="updated-lab">Updated lab</h1>
<p>I&rsquo;ve updated the <a href="https://github.com/jmcmillan1873/autoscaling-with-imagebuilder">autoscaling_with_imagebuilder</a> lab to reflect this. The Lambda updater and its associated EventBridge rule have been removed. The distribution config now handles the Launch Template update natively, as it always should have.</p>
<p>If you previously deployed the lab with the Lambda approach, you can safely remove those resources and update your distribution configuration. The SSM Parameter pattern remains, it&rsquo;s still useful for the Terraform bootstrapping needs.</p>
<h1 id="thoughts">Thoughts</h1>
<p>I&rsquo;ll be honest, this is a satisfying update to write. The original solution worked, and I do think it was a reasonable approach given the constraints at the time. But it always felt like I was compensating for something that should have been a native capability.</p>
<p>The fact that AWS have addressed this is a good sign. It suggests they listened to the feedback (and I suspect I wasn&rsquo;t the only one who ran into this). It also validates the frustration I had - if the fix was to make Distribution Settings preserve existing config, then the previous behaviour <em>was</em> the problem, not my expectations.</p>
<p>One less Lambda in the world. Now if AWS could just make their <a href="https://aws.mcmillearn.net/posts/half-baked-lifecycles/">Lifecycle Policies</a> stop nuking AMIs that are still in use, I might be able to retire the <em>other</em> Lambda too. A man can dream.</p>
<h2 id="additional-resources">Additional Resources</h2>
<ul>
<li><a href="https://aws.mcmillearn.net/posts/half-baked-images/">Half Baked Images - original blog post</a></li>
<li><a href="https://aws.mcmillearn.net/posts/half-baked-lifecycles/">Half Baked Lifecycles - follow up blog post</a></li>
<li><a href="https://github.com/jmcmillan1873/autoscaling-with-imagebuilder">Updated Auto Scaling Group with EC2 Image Builder Lab</a></li>
<li><a href="https://docs.aws.amazon.com/imagebuilder/latest/userguide/dist-using-launch-template.html">EC2 Image Builder - Configure AMI distribution with a launch template</a></li>
</ul>
<hr>
<p><em>This post is part of my AWS technical blog series. Found this helpful? Connect with me on <a href="https://www.linkedin.com/in/john-mcmillan-aaa4274/">LinkedIn</a> or check back here for more AWS content and discussions.</em></p>
]]></content:encoded><category>AWS</category><category>EC2 Image Builder</category><category>Auto Scaling Group</category><category>AWS</category><category>EC2</category><category>Auto Scaling Group</category><category>EC2 Image Builder</category><category>AWS EC2 Image Builder</category><category>AWS Auto Scaling Group</category><enclosure url="https://aws.mcmillearn.net/images/half-baked-update-title.png" type="image/png"/></item><item><title>Why Cloud sovereignty suddenly matters.</title><link>https://aws.mcmillearn.net/posts/aws-eu-sov-cloud/</link><pubDate>Tue, 27 Jan 2026 09:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/posts/aws-eu-sov-cloud/</guid><description>What is Sovereignty and do your need it?</description><content:encoded><![CDATA[<h1 id="disclaimer">Disclaimer</h1>
<p><em><strong>AKA: Are you sure you want to read this?</strong></em><br>
Okay - so this isn&rsquo;t my typical technical approach to blogging.
This topic is regulatory heavy - unavoidably so if you&rsquo;re to understand and navigate it. You have been warned :).</p>
<h1 id="intro">Intro</h1>
<p>You might have noticed the world feels slightly less keen on being stable and predictable. Geopolitics, trade, and national interest are clashing more openly. Cloud computing is an area where we see some of the downstream consequences as a result. The &lsquo;Cloud&rsquo; has a truly global presence, and so perhaps it&rsquo;s unsurprising that it isn&rsquo;t immune from politics. Companies like AWS made it possible for organisations of any size to start small and grow rapidly to a huge global scale - &ldquo;reach your clients wherever they are&rdquo; - that wasn&rsquo;t by magic. It was <em>because</em> the cloud went global.</p>
<p>The cloud, being global by design, blurred many traditional boundaries. In doing so it made possible faster innovation and global reach to anyone with a good idea and the will to act on it. It also quietly blurred some legal and jurisdictional boundaries.</p>
<p>Over the past 10 years, laws like the <a href="https://www.justice.gov/criminal/cloud-act-resources">CLOUD Act</a> and before that, the PATRIOT Act, have forced business leaders and their legal teams to contemplate some uncomfortable, but legitimate, questions. e.g. <em>&ldquo;Who can access my data, and under what circumstances?&rdquo;</em>.</p>
<p>Some of these laws create the possibility that their corporate data, user data, or intellectual property held by a US based service provider, could be subject to a lawful access request from US legal authorities. Potentially without the client knowing.</p>
<p>Companies like AWS have offered strong assurances that they&rsquo;ll challenge overreach and advise any clients when this happens if they&rsquo;re legally permitted to. And they provide strong mechanisms to protect your data from unauthorised access, e.g. Nitro, which we&rsquo;ll take about later.
See <strong>&ldquo;How does AWS handle Law enforcement requests?&rdquo;</strong> in their <a href="https://aws.amazon.com/compliance/cloud-act/">CLOUD Act FAQs</a>.</p>
<p>But AWS don&rsquo;t need me to fight their corner. They have a few more resources than I have to throw at that.</p>
<p>Either way, Sovereign Cloud is here. It&rsquo;s up to us as IT professionals to understand what that means for us, our employers, clients, and their users.</p>
<p>In this blog I&rsquo;ll look at what Sovereignty is, what it&rsquo;s not - and how to avoid common missteps, e.g. that data residency equates to digital sovereignty. It doesn&rsquo;t.</p>
<h2 id="sovereignty">Sovereignty</h2>
<p>The Oxford English dictionary defines Sovereignty as&hellip;. no, that would be awful. :)</p>
<p>Digital Sovereignty is about a few things.</p>
<ul>
<li>Control - which outsiders can impact you, derail your business, or access your data?</li>
<li>Jurisdiction - which legal systems have authority over your data, applications, and services?</li>
<li>Auditability - who&rsquo;s done what, where, and when - and can I prove that to auditors?</li>
<li>Security - how do I protect my data and access to my systems?</li>
<li>Accountability - who am I answerable to? Users, regulators, shareholders, etc</li>
<li>Residency - where is my data located?</li>
<li>Operations - who manages, operates, and maintains my systems?</li>
</ul>
<p>You might be thinking, <em>some of those overlap</em>. Yup. The factors that contribute to Sovereignty do overlap somewhat.</p>
<p>So think about it this way:<br>
If your important data is <strong>classified correctly</strong>, you know <strong>where it is</strong> and it&rsquo;s <strong>confined to where you want</strong> it to be, you understand how to <strong>control access</strong> to it, you can audit <strong>who&rsquo;s had access</strong>, you know <strong>who you&rsquo;re subject to</strong> in terms of regulatory bodies &amp; compliance frameworks, <strong>who needs access</strong> to your systems, <strong>who <em>has</em> access</strong> to your systems, then you&rsquo;re in a good position.</p>
<p>If you answered positively to those then you&rsquo;re well on the way to understanding what Sovereignty is, and which aspects apply to you.</p>
<h3 id="what-sovereignty-isnt">What Sovereignty isn&rsquo;t</h3>
<p><strong>Data residency (location) is not Digital Sovereignty.</strong>
It&rsquo;s likely that some clients will ask for Sovereign solutions when in actual fact, they&rsquo;re really concerned about data residency and security. Helping those clients understand the difference between where data can be stored and secured (Residency) and who can legally compel access (legal Jurisdiction) is a difference to be understood.</p>
<p><em><strong>Make my solution fully Sovereign - retrospectively</strong></em><br>
If a client states that they want to keep their existing solution but &ldquo;make it Sovereign&rdquo;&hellip; that&rsquo;s not necessarily impossible, but it&rsquo;s tricky. Sovereignty is a design time decision. To achieve the best possible sovereign posture for your solution, the design needs to factor in the Sovereignty objectives and address each where possible. (good luck with Supply Chain Sovereignty).</p>
<p><em><strong>I want to increase my Sovereignty without compromise</strong></em><br>
For some clients, the appeal of being sovereign and independent from foreign owned companies will be appealing.</p>
<h2 id="eu-sentiment">EU Sentiment</h2>
<p>I won&rsquo;t spend time talking about the <a href="https://www.europarl.europa.eu/RegData/etudes/ATAG/2020/652073/EPRS_ATA(2020)652073_EN.pdf">Schrems II ruling</a>, but I&rsquo;d recommend having a look into that for some context - it gets to the heart of some of the trust issues that are at the centre of the CLOUD Act, conflicting national interests, and the EU position on the subject.<br>
Suffice it to say, the EU have been concerned for some time about their belief that a lack of equivalence exists in data protection laws between the EU and US, and this creates risk for EU based organisations using US based cloud providers.</p>
<p>On the point of <strong>trust</strong> — it’s an important distinction to make, but the European Commission created the EU Sovereign Cloud framework because the EU considers foreign legal reach to be a risk that could undermine things like GDPR, not because it distrusts cloud technology.</p>
<p>Organisations that are subject to foreign legal obligations, such as the US CLOUD Act, may be viewed through a sovereignty lens as presenting a potential avenue for legal overreach.</p>
<h3 id="enter-the-eu-cloud-sovereign-framework">Enter the EU Cloud Sovereign Framework</h3>
<p>It&rsquo;s no surprise then that the EU decided to produce a framework that helps organizations assess their current sovereignty posture and to provide some clear guidance and metrics in the form of the <a href="https://commission.europa.eu/document/download/09579818-64a6-4dd5-9577-446ab6219113_en?filename=Cloud-Sovereignty-Framework.pdf">EU Cloud Sovereignty Framework</a>.</p>
<h3 id="what-does-the-framework-define">What does the framework define</h3>
<p>It&rsquo;s definitely worth reading the <a href="https://commission.europa.eu/document/download/09579818-64a6-4dd5-9577-446ab6219113_en?filename=Cloud-Sovereignty-Framework.pdf">framework</a>, it&rsquo;s actually very concise considering what it is, but in the meantime, I&rsquo;ll summarise here:</p>
<h4 id="seal-scores">SEAL scores</h4>
<p>The document explains the concept of SEAL scores - <strong>Sovereignty Effectiveness Assurance Level</strong>.<br>
There are eight contributing &lsquo;Sovereignty Objectives&rsquo;, and a related formula that accounts for different weightings, that provide the inputs for calculating the SEAL score of a platform or solution.</p>
<h4 id="sovereign-objectives-the-factors">Sovereign Objectives (the factors)</h4>
<ul>
<li><strong>Strategic Sovereignty</strong> - This assesses the supplier&rsquo;s ownership stability, governance influence, &amp; alignment with EU Strategic priorities.</li>
<li><strong>Legal &amp; Jurisdictional Sovereignty</strong> - Evaluates the legal environment, exposure to foreign authority, &amp; enforceability of rights.</li>
<li><strong>Data &amp; AI Sovereignty</strong> - Focuses on how data and AI services are secured, protected, controlled, where they&rsquo;re located, and where they&rsquo;re processed from.</li>
<li><strong>Operation Sovereignty</strong> - Assess the ability of EU based organizations to support, patch, maintain, service, and evolve a technology or solution independent of non-EU control.</li>
<li><strong>Supply Chain Sovereignty</strong> - Evaluates the geographic origin, transparency, and resilience of the supply chain, with a specific focus on the extent critical components are or aren&rsquo;t controlled by the EU.</li>
<li><strong>Technology Sovereignty</strong> - Considers the degree of transparency and independence in the technology stack. Can EU organizations, operate the technology, audit it, and evolve it without lock-in to foreign owned systems.</li>
<li><strong>Security &amp; Compliance Sovereignty</strong> - Assesses the extent to which security operations, compliance obligations, and resilience are controlled within the EU, without reliance on foreign jurisdictions.</li>
<li><strong>Environmental Sustainability</strong> - This assesses the autonomy &amp; resilience of cloud services over the long term in relation to energy usage, dependency, and raw material security. (aka - can you be Sovereign if you&rsquo;re not energy independent and someone else can switch off the lights?)</li>
</ul>
<p>The framework goes into more detail about some of the specific examples and criteria they look for and assess in each of those factors. At this stage I just wanted to give you some relevant context to frame the Hyperscaler response, where it works and where it doesn&rsquo;t.</p>
<h2 id="hyperscaler-response">Hyperscaler response</h2>
<p>I&rsquo;ve stated outright that I personally operate in the AWS arena - other hyperscalers are taking this seriously too - but I&rsquo;ll talk about how AWS are now facing up to the sovereignty challenge in the EU.</p>
<h3 id="enter-eu-aws-sovereign-cloud">Enter EU AWS Sovereign Cloud.</h3>
<p>AWS first announced plans for their EU Sovereign Cloud business in October 2023. In January 2026, following €8 billion investment, AWS formally announced general availability of its EU Sovereign Cloud located in Germany.</p>
<blockquote>
<p>&ldquo;<em>The AWS European Sovereign Cloud is a new, independent cloud for Europe entirely located within the European Union (EU), designed to help customers meet their evolving sovereignty requirements.</em>&rdquo;</p>
</blockquote>
<p>AWS conceived and built the EU Sovereign Cloud to be &ldquo;Sovereign-by-design&rdquo;. AWS have worked with various EU organizations to gather the needs of clients who are interested in Sovereign Cloud. In that sense it&rsquo;s a genuine attempt at a purpose built Sovereign Cloud. You can see the thought that&rsquo;s gone into that when you look at service availability. For example, at the time I write this there are only two Foundational Models available in <code>eusc-de-east-1</code> under Amazon Bedrock - &lsquo;Nova Lite&rsquo; and &lsquo;Nova Pro&rsquo;.<br>
Is that just taking time to roll out services and features, or is it regulation making vendors think carefully and seriously about risk, compliance, &amp; governance?</p>
<p>If you&rsquo;re already familiar with AWS, then EU Sovereign Cloud won&rsquo;t initially look any different. In many respects it looks and behaves like any other AWS Region.
As with AWS Regions, there&rsquo;s variation in number of Availability Zones (<code>eusc-de-east-1</code> has two), variation in service/feature availability, and pricing differences.</p>
<p>The sole purpose of AWS&rsquo;s investment in the EU Sovereign Cloud is to offer, as close as is achievable, a truly sovereign platform for the EU. In their words, from the</p>
<p>Does it manage that? Let&rsquo;s look first at how AWS&rsquo; EU Sovereign cloud measures up to the framework:</p>
<p><strong>Strategic Sovereignty</strong></p>
<p>AWS EU Sovereign Cloud is owned by &ldquo;AWS European Sovereign Cloud GmbH&rdquo;, a German registered company - who are themselves 100% owned the US based Amazon.
Although, the AWS EU board is entirely based in the EU though and comprised of EU Citizens. This means they&rsquo;ll be able to get involved in EU initiatives, and they&rsquo;ll be able to sustain the service in the EU even if the US government did something unthinkable like instructing Amazon to stop offering services in the EU. AWS have made some pretty firm and public commitments here, saying:</p>
<blockquote>
<p><a href="https://aws.eu/faq/#governance-and-leadership--1hs32zh"> &ldquo;<em>AWS established an independent advisory board for the AWS European Sovereign Cloud, legally obligated to act in the best interest of the AWS European Sovereign Cloud. Reinforcing the sovereign control of the AWS European Sovereign Cloud, the advisory board will consists of five members, all EU citizens residing in the EU, including at least two independent board members who are not affiliated with Amazon. The advisory board will act as a source of expertise and provide accountability on sovereignty-related aspects of the AWS European Sovereign Cloud operations, including strong security and access controls and the ability to operate independently in the event of disruption.</em>&rdquo;</a></p>
</blockquote>
<p>Therefore, compared to AWS Public Cloud, AWS EU Sovereign Cloud offers a significant improvement in this regard, but it wouldn&rsquo;t score as highlight as truly owned EU organization with no ties or ownership outside of the EU.</p>
<p><strong>Legal &amp; Jurisdictional Sovereignty</strong><br>
AWS have done a good job of keeping the &ldquo;AWS European Sovereign Cloud GmbH&rdquo; entity at arm&rsquo;s length. Physically and logically isolated from wider AWS. All staff are EU residents, and the board are entirely EU citizens. AWS state in their Sovereign Cloud overview that:</p>
<blockquote>
<p>As part of the technical design, access to the AWS European Sovereign Cloud physical infrastructure and
logical system is managed by Qualified AWS European Sovereign Cloud Staff and can only be granted to
Qualified AWS European Sovereign Cloud Staff located in the EU. AWS European Sovereign Cloud restricted data will not be accessible, including to AWS employees, from outside the EU.</p>
</blockquote>
<p>So only AWS staff employed in the EU have any sort of access.<br>
Is that enough to say there isn&rsquo;t a loophole that means the FBI can&rsquo;t petition AWS in the US to get at data? It appears that would require compelling EU employees to brake local law, which doesn&rsquo;t seem likely.<br>
Truthfully - I think we&rsquo;ll need to see this tested in the courts to know whether this is as robust as we hope or not, but we might be waiting a while&hellip;</p>
<blockquote>
<p><a href="https://aws.amazon.com/compliance/cloud-act/"><em>The CLOUD Act has resulted in zero disclosures of AWS enterprise or government customer content stored outside the U.S. to the U.S. government, since we started reporting the statistic in 2020.</em></a></p>
</blockquote>
<p><strong>Data &amp; AI</strong><br>
AWS EU Sovereign Cloud offer a lot to assure clients in this respect, whether on AWS EU Sovereign Cloud or AWS Public Cloud. There are strong controls in places to allow clients to secure access and auditing to their data to a high degree.
Factor in things like <a href="https://aws.amazon.com/ec2/nitro/">AWS Nitro</a> which provides strong isolation between tenants. In fact, Nitro Isolation prevents anyone other than the client accessing instances - even AWS Staff have no access to customer Instances. Nitro offloads networking, storage, and management functions to dedicated hardware, it significantly reduced the attack surface of the Hypervisor, and all admin access is prohibited.<br>
AWS EU Sovereign Cloud is independently audited and verified to various compliance standards - including attestations for Nitro.</p>
<p><strong>Operational Sovereignty</strong><br>
AWS EU Cloud staff is entirely comprised of EU residents, and AWS have committed that this will be EU citizens located in the EU shortly.
No non-EU residents will have access to the EU Sovereign Infrastructure.</p>
<p>Who clients choose to support the solutions they deploy on EU Sovereign Cloud is entirely in their control - but AWS have closed the door on this one.</p>
<p><strong>Supply Chain Sovereignty</strong><br>
Okay. This is a difficult one to check and satisfy. The EU framework refers to a few aspects of supply chain including where key components are manufactured. But it doesn&rsquo;t clarify what it means by <em>key</em>.<br>
Think of all the devices involved in a typical solution (regardless of where it is) - cpu, PSU, motherboards, network cards, storage devices, adapters, cables, switches, firewalls, gpus, etc - and how many chips and electronics in each of those?</p>
<p>I couldn&rsquo;t find much clarity in AWS published material on this point.
AWS does publish <a href="https://sustainability.aboutamazon.com/amazon-supply-chain-standards-english.pdf">Supply Chain Standards</a> which includes relevant information - but it&rsquo;s quiet on <em>where</em> components come from.</p>
<p>My personal belief is that this is a complicated topic even for behemoth organizations like AWS - and the reality is the growing demand for full digital sovereignty shines on a light on an interconnected, complex, global supply chain - which means this is currently an extremely difficult lens to be viewed as Sovereign.</p>
<p>Organizations will have to make individual decisions about how far they progress in this direction, or except their partners and suppliers to.</p>
<p><strong>Technology Sovereignty</strong><br>
Transparency, auditability, and avoiding proprietary lock in is the objective here. i.e. Are you using software that can be transparently audited? Can you influence the direction of the code. Could someone else prevent you using that code?</p>
<p>Many AWS services are based on Open Source software, or software that has functional Open Source equivalents, and lock-in is something to be managed anyway when deploying to a cloud - but there are ways to manage that so you&rsquo;re not entirely dependent on a cloud platform - especially for containerised workloads.</p>
<p>But AWS in the USA are still responsible for the upstream code. So even though the EU is independent in some ways, it&rsquo;s still seen to be reliant on the US entity for that upstream code.</p>
<p>Again, Organizations will have to decide on the right balance of convenience and compliance in terms of transparency, and whether the improved independence of the AWS EU Sovereign cloud is sufficient.</p>
<p><strong>Security &amp; Compliance Sovereignty</strong><br>
AWS have traditionally done well in this space. The AWS EU cloud is no exception. It has the same security and permissions models as the AWS Public Cloud. e.g. IAM, Organizations, SCPs, etc - but is regionally scoped, implemented, and operated.</p>
<p>Services like Amazon GuardDuty &amp; Security Hub are partially available - some features are on tap today; others planned for the EU Cloud. Check <a href="https://builder.aws.com/build/capabilities/explore?f=eJyrVipOzUlNLklNCUpNz8zPK1ayUoqOUUotLU7WTUnVTU0sLtE1jFGKVdKBK3QsS8zMSUzKzMksqQSqdsyrVEARqgUA4l8dog&amp;tab=service-feature">Builder Centre</a> for service/feature availability in <code>eusc-de-east-1</code>.</p>
<p>Amazon Nitro enables <a href="https://docs.aws.amazon.com/whitepapers/latest/security-design-of-aws-nitro-system/no-aws-operator-access.html">no operator access</a> - coupled with no access to anyone outside the EU - this is a strong position for AWS EU Sovereign Cloud.</p>
<p><strong>Environmental Sustainability</strong><br>
AWS do well here also. AWS have a <a href="https://www.aboutamazon.com/news/aws/aws-data-center-ai-circularity">strong message</a> on circular economy, energy efficient data centre, and <a href="https://aws.amazon.com/sustainability/">sustainability</a>. They&rsquo;re on track to meet targets to be water positive by 2030 and net-zero carbon by 2040.</p>
<p>This is an area that&rsquo;s weighted lowly in the EU framework though - at only 5%.</p>
<h2 id="aws-eu-sovereign-cloud-vs-aws-public-cloud">AWS EU Sovereign Cloud vs AWS Public Cloud</h2>
<p>How do the two platform compare then?<br>
As already pointed out, where a service exists on EU Sovereign Cloud, it&rsquo;s</p>
<h3 id="feature--usage-comparison">Feature / usage comparison</h3>
<p>At a high level <em>some</em> of the differences appear quite quickly:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>AWS Public Cloud</th>
          <th>AWS EU Sovereign Cloud</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>What it&rsquo;s optimised for</td>
          <td>Speed, Scale, Global Reach</td>
          <td>Legal &amp; operational Sovereignty (EU)</td>
      </tr>
      <tr>
          <td>Strong Points</td>
          <td>Rapid innovation, Full AWS Service catalogue, Huge elastic scale, Low cost to entry</td>
          <td>EU Only operations, Reduced foreign legal exposure, AWS Native experience</td>
      </tr>
      <tr>
          <td>Limitations</td>
          <td>Jurisdictional ambiguity, cost predictability requires focus, easy to over engineer</td>
          <td>Smaller &amp; slower moving service set, narrow Geographic scope, High cost than AWS Public Regions</td>
      </tr>
      <tr>
          <td>Use cases</td>
          <td>Digital native apps, SaaS platforms, Data analytics &amp; AI/ML, Global customer solutions, Strong governance &amp; compliance requirements</td>
          <td>Public Sector &amp; Critical National Infrastructure, Strong Sovereignty Requirements, FIS, National Data platforms, High assurance workloads</td>
      </tr>
      <tr>
          <td>Bad fits</td>
          <td>Strong EU Sovereignty Requirements, Data platforms subject to strict national control</td>
          <td>Non Sovereign workloads with tight budgets, Rapid global expansion</td>
      </tr>
  </tbody>
</table>
<h3 id="sovereignty-effectiveness-assurance-level">Sovereignty Effectiveness Assurance Level</h3>
<p>The formula to calculate SEAL Scores is explained in the EU Sovereign Compliance Framework - refer to that for details.  To my mind scoring is subjective to a point You can review the framework, review the platform capabilities, and decide how close you feel there are or aren&rsquo;t and score accordingly.</p>
<p>I won&rsquo;t show my working here for various mandated commercial reasons - but I&rsquo;ll share the outcomes. My <strong>personal opinion</strong> after consideration resulted in me viewing the platforms to be:</p>
<ul>
<li>AWS Public Cloud - SEAL 2</li>
<li>AWS EU Sovereign Cloud - SEAL 3</li>
</ul>
<h2 id="final-thoughts">Final thoughts</h2>
<p>The <a href="https://aws.eu">AWS EU Sovereign Cloud</a> is a strong option for clients with strong Sovereignty requirements, but it isn&rsquo;t the right choice for everyone. It&rsquo;s not a one-size-fits-all solution.</p>
<p>Given AWS prices usually fluctuate by region, it&rsquo;s difficult to accurately compare the cost difference with EU cloud. I deploy in <code>eu-west-1</code> &amp; <code>eu-west-2</code> a lot, so I compared with my costs there and found the price was in the region of 15-20% more in <code>eusc-de-east-1</code>. That&rsquo;s enough to be noticed and given the improved Sovereignty posture over AWS Public cloud, that feels reasonable to me. It also feels <em>just high enough</em> that it&rsquo;ll help AWS clients decide if they want improved compliance, security, and data control (which they could likely do in their existing environments) or whether they <em><strong>really</strong></em> need a full sovereign platform.</p>
<p>The highest SEAL rating is &lsquo;4&rsquo;. I think as we see other platforms being assessed - 4 will be awarded rarely. The framework doesn&rsquo;t easily allow for that in a global economy with global supply chains.</p>
<p>Both AWS Cloud and Sov Cloud lose points in &lsquo;Strategic&rsquo;, &lsquo;Supply chain&rsquo;, and &lsquo;Technology&rsquo; sovereignty objectives.<br>
However, AWS EU Sov cloud does significantly improve the position on &lsquo;Legal &amp; Jurisdictional&rsquo;, &lsquo;Operational&rsquo;, &amp; &lsquo;Data &amp; AI&rsquo; sovereignty objectives.</p>
<p>The small caveat is that the scoring of these is somewhat subjective (hence I stressed above my <strong>personal opinion</strong>) and clients will decide themselves what they need.  It&rsquo;s our job to understand the differences, articulate those, and design &amp; build accordingly.</p>
<h2 id="additional-resources">Additional Resources</h2>
<ul>
<li><a href="https://commission.europa.eu/document/download/09579818-64a6-4dd5-9577-446ab6219113_en?filename=Cloud-Sovereignty-Framework.pdf">EU Cloud Sovereignty Framework</a></li>
<li><a href="https://aws.amazon.com/blogs/security/five-facts-about-how-the-cloud-act-actually-works/">Five facts about how the CLOUD Act actually works</a></li>
<li><a href="https://d1.awsstatic.com/onedam/marketing-channels/website/aws/en_US/whitepapers/compliance/Overview_of_the_AWS_European_Sovereign_Cloud.pdf">Overview of the AWS EU Sovereign Cloud</a></li>
</ul>
<hr>
<p><em>This post is part of my AWS technical blog series. Found this helpful? Connect with me on <a href="https://www.linkedin.com/in/john-mcmillan-aaa4274/">LinkedIn</a> or check back here for more AWS content and discussions.</em></p>
]]></content:encoded><category>AWS</category><category>Sovereign Cloud</category><category>Compliance</category><category>AWS</category><category>EU Sov Cloud</category><category>EU Sovereign Cloud</category><category>Sovereign</category><category>Compliance</category><category>AWS AWS EU Sovereign Cloud</category><category>AWS AWS Nitro</category><enclosure url="https://aws.mcmillearn.net/images/aws-eu-sov-banner.png" type="image/png"/></item><item><title>Half Baked Lifecycles</title><link>https://aws.mcmillearn.net/posts/half-baked-lifecycles/</link><pubDate>Fri, 02 Jan 2026 09:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/posts/half-baked-lifecycles/</guid><description>How to lifecycle-manage AMIs produced by Amazon EC2 Image Builder</description><content:encoded><![CDATA[<h1 id="intro">Intro</h1>
<p>In my previous post, <a href="https://aws.mcmillearn.net/posts/half-baked-images/">Half Baked Images</a> I walked through building hardened AMIs using Amazon EC2 Image Builder, automating the pipeline, and wiring it into Launch Templates for Auto Scaling Groups. I touched on some aspects that felt <em>almost, but not quite</em> useful. In this post I&rsquo;ll talk about my attempts to keep on top of the images produced and why (spoiler!) I ultimately chose Lambda over built-in Image Builder functionality.</p>
<h2 id="task-stop-amis-and-snapshots-from-piling-up">Task: Stop AMIs and snapshots from piling up</h2>
<p>AWS has an answer for that, of course: Image Builder lifecycle policies.<br>
This should be easy (AKA Foreshadowing).</p>
<p>When asked &ldquo;<em>What does Lifecycle management in Image Builder do?</em>&rdquo;, Amazon Q gave this answer:</p>
<blockquote>
<p>&ldquo;The lifecycle manager implements automated rules to manage your image resources throughout their lifecycle, helping ensure image freshness while minimizing costs for underlying infrastructure like snapshot storage for AMIs or ECR repository storage for container images.&rdquo;</p>
</blockquote>
<p>I understand all those words so this is going to be easy peasy.</p>
<h3 id="lifecycle-management-made-confusing">Lifecycle management made&hellip; confusing.</h3>
<p>What I didn’t anticipate was how surprisingly awkward it would be to implement what felt like a very simple solution.</p>
<p>Unfortunately, the behaviour of those policies was less intuitive than the name suggested&hellip; to me at least.</p>
<p>This post is about the assumptions I made, why they were wrong, what Image Builder lifecycle actually does, and why I ultimately replaced it with a small Lambda function that behaved better for my use case.</p>
<h3 id="initial-approach">Initial approach</h3>
<p>I started with a simple goal:</p>
<blockquote>
<p>Retain the last 5 AMIs produced by Image Builder.<br>
Delete anything older, including associated snapshots.</p>
</blockquote>
<p>Image Builder has a native lifecycle feature. Terraform exposes it cleanly.
This felt so obvious that I didn’t question it. The console UI even said things like:</p>
<p><img alt="Keep the last 5" loading="lazy" src="/posts/half-baked-lifecycles/keep5.png"></p>
<p>So I configured a lifecycle policy in terraform like this:</p>
<pre tabindex="0"><code>resource &#34;aws_imagebuilder_lifecycle_policy&#34; &#34;retain_5_images&#34; {
  name           = &#34;${var.project}-lifecycle-policy&#34;
  description    = &#34;Retain only the 5 most recent AMIs created by Image Builder&#34;
  execution_role = aws_iam_role.imagebuilder_role.arn

  resource_type = &#34;AMI_IMAGE&#34; 

  resource_selection {
    tag_map = {
      Project   = var.project
      ManagedBy = &#34;AWSImageBuilder&#34;
    }
  }

  policy_detail {
    action {
      type = &#34;DELETE&#34;
      include_resources {
        amis       = true
        containers = false
        snapshots  = true
      }
    }

    filter {
      type  = &#34;COUNT&#34; # or &#34;COUNT&#34;
      value = &#34;5&#34;
    }
  }
}
</code></pre><p><strong>Note:</strong> This wasn&rsquo;t my first attempt, this was the first that ran correctly.<br>
It took me a week to get it to run. See Assumption #1.</p>
<p>Even now, recovering from the savage AMI cull of &lsquo;25, I still read that code and think the policy should:</p>
<ul>
<li>Take the <code>DELETE</code> action for resources type &ldquo;AMI_IMAGE&rdquo;&hellip;</li>
<li>also including resource types AMIs and snapshots, for those AMI_IMAGEs that matched&hellip;</li>
<li>tags <code>Project</code> &amp; <code>ManagedBy</code>, whilst retaining&hellip;</li>
<li>Filter: COUNT = 5</li>
</ul>
<p>Therefore I expected the outcome to be:</p>
<blockquote>
<p>Find Images were <code>Project = var.project</code> and <code>ManagedBy = &quot;AWSImageBuilder&quot;</code><br>
If there are ≤5 images that match that criteria → delete nothing<br>
If there are 6 → delete the oldest 1<br>
If there are 18 → delete the oldest 13</p>
</blockquote>
<p>etc. Simple. Predictable. Safe.</p>
<p>That is not what happened.<br>
But what went wrong?</p>
<h2 id="assumption-is-the-mother-of-all-fowl-ups">Assumption is the mother of all&hellip; fowl ups.</h2>
<h3 id="assumption-1">Assumption #1</h3>
<p>My first assumption was that I would have some control over when the lifecycle management would run. Similar to controlling the pipeline schedule, I&rsquo;d be able to control a lifecycle management schedule.</p>
<p>e.g. I&rsquo;d be able to define a cron schedule or trigger it to run immediately after a new image became <code>AVAILABLE</code>. Nope.</p>
<p>Image Builder Lifecycle policies run on an internal AWS schedule, completely decoupled from the triggers or control I expected. There is also no &ldquo;Run now&rdquo; button to force an evaluation.</p>
<p>This had two consequences that significantly impacted my use case:</p>
<ol>
<li>
<p><strong>Testing is non-linear.</strong><br>
Unsurprisingly my lifecycle didn&rsquo;t work on the first try. So, when I attempted to fix it, I had to wait 24 hours to see if that had worked.<br>
Testing, that could have taken a couple of hours, took <em><strong>days</strong></em> to work through as a result of this approach.</p>
</li>
<li>
<p><strong>Troubleshooting was complex and misleading</strong><br>
I was working on a few aspects of the code simultaneously, so by the time my lifecycle ran each day I had usually made several changes.</p>
<ul>
<li>Tags had been added or changed</li>
<li>IAM permissions were tightened</li>
<li>Images components were changing, meaning recipes had been updated.</li>
</ul>
</li>
</ol>
<p>The combination of those two realities made it very difficult to figure out cause and effect: Was lifecycle management failing because I&rsquo;d borked the config? Was it related to my tagging changes? or the new recipe?<br>
<em>sigh.</em></p>
<h3 id="assumption-2">Assumption #2</h3>
<p>My second assumption was that the lifecycle policy in Image Builder relates to the images built by Image Builder&hellip; that is to say - the AMIs.</p>
<p>Sensible assumption? I thought so&hellip; and if you did too&hellip; join me in my pit of despair because, as we&rsquo;ll get to in a moment, this was <strong>not</strong> the case.</p>
<h3 id="assumption-3">Assumption #3</h3>
<p>My third, and what proved to be my most costly assumption, was that I&rsquo;d provided enough details to safely identify the images to be deleted and that only those Images would be deleted. That this turned out to be incorrect was the real sore point - this one wasn&rsquo;t easy to predict or see coming until it went wrong.</p>
<h2 id="so-what-actually-happened">So what actually happened?</h2>
<p>When my lifecycle policy ran, it removed <strong>all</strong> AMIs. Everything.<br>
Including AMIs <em><strong>still</strong></em> in use by my Auto Scaling Group&rsquo;s Launch Template.<br>
Nothing useful remained. Zero AMIs, zero snapshots.</p>
<p>Thankfully this occurred in a lab environment so wasn&rsquo;t too impactful&hellip; but it did give me chills. I looked over the code, at the resources, and the lifecycle policy in the console, etc. It all looked correct. It <em>seemed</em> to make sense - but here I was with no useable images left.<br>
And at the moment of discovering this issue, even from that point, it took a good few hours to understand why it had gone so badly wrong.</p>
<p>To say I was a bit surprised at the outcome is putting it mildly.<br>
This could not be allowed to happen in an environment that matters to me.</p>
<h3 id="where-did-the-assumptions-fall-down">Where did the assumptions fall down?</h3>
<ol>
<li>
<p>Not having control over the scheduling of the lifecycle policy was simultaneously a relatively minor issue <strong>and</strong> almost enough to be a deal breaker on its own. Who&rsquo;s happy waiting 24 hours to test configuration changes in a lab? Fail fast isn&rsquo;t an option, if you have to wait 24 hours to test each change.</p>
</li>
<li>
<p>My assumption that my lifecycle policy was targeting my AMI&rsquo;s directly was incorrect. This one <em>really</em> mattered.<br>
EC2 Image Builder Lifecycle Policies target Image builder resources, e.g. things with arns like <code>arn:aws:imagebuilder:eu-west-2:123456789012:image/custom-linux-build/1.0.6/3</code>.<br>
This is a subtle but important distinction to understand when you&rsquo;re thinking about how Image Builder Lifecycle policies identify targets. You can, as I did in the code above, state that it should include <code>amis = true</code> &amp; <code>snapshots = true</code>. But, again, the AMIs and snapshots are secondary in this.</p>
</li>
<li>
<p>Assuming I&rsquo;d provided the correct selection criteria was compounded by the previous assumption about what the lifecycle policies target.<br>
I mentioned that after my lifecycle policy ran I had zero AMI&rsquo;s and zero snapshots.<br>
But I <em><strong>did</strong></em> still have 5 &ldquo;images&rdquo;. I still had 5 <em><strong>Image Builder</strong></em> Image&rsquo;s retained. Although the AMI&rsquo;s that they pointed to were gone.</p>
</li>
</ol>
<p>Yup, I swore inwardly at this point.</p>
<h3 id="bringing-it-home">Bringing it home</h3>
<p>I asked Amazon Q why it would do this (to me), and in its reply it had this little unhelpful truth bomb:</p>
<blockquote>
<p>&ldquo;Even if the Image Builder Image resources were retained, if your policy had:<code>&quot;includeResources&quot;: { &quot;amis&quot;: true, &quot;snapshots&quot;: true }</code>
<strong>This would delete the underlying AMIs and snapshots while potentially keeping the Image Builder metadata.</strong>&rdquo;</p>
</blockquote>
<p>I swore outwardly at this point. Loudly.</p>
<p>This ia an awful failure condition by any stretch. I accept that this was my configuration, and <strong>I</strong> am responsible for this, it still feels like this was a booby trap waiting to go off rather than something you could reasonably foresee &amp; avoid.</p>
<h2 id="so-what-did-i-do-about-it">So what did I do about it</h2>
<p>After a fair amount of frustration, confusion, and lost AMIs I decided to abandon the Image Builder lifecycle policy approach and turned to AWS Lambda - what can&rsquo;t Lambda fix?</p>
<p>I created a small function that I deployed using Terraform, as follows:</p>
<pre tabindex="0"><code>import os
import json
import boto3
import logging
from datetime import datetime, timezone

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ec2 = boto3.client(&#34;ec2&#34;)

RETAIN_COUNT = int(os.getenv(&#34;RETAIN_COUNT&#34;, &#34;5&#34;))
PROJECT_TAG_VALUE = os.getenv(&#34;PROJECT_TAG_VALUE&#34;, &#34;labpoc&#34;)
MANAGED_BY_VALUE = os.getenv(&#34;MANAGED_BY_VALUE&#34;, &#34;AWSImageBuilder&#34;)
LAUNCH_TEMPLATE_ID = os.getenv(&#34;LAUNCH_TEMPLATE_ID&#34;)  # required for protection
DRY_RUN = os.getenv(&#34;DRY_RUN&#34;, &#34;true&#34;).lower() == &#34;true&#34;


def _parse_dt(s: str) -&gt; datetime:
    return datetime.fromisoformat(s.replace(&#34;Z&#34;, &#34;+00:00&#34;)).astimezone(timezone.utc)


def _list_matching_amis():
    filters = [
        {&#34;Name&#34;: &#34;tag:Project&#34;, &#34;Values&#34;: [PROJECT_TAG_VALUE]},
        {&#34;Name&#34;: &#34;tag:ManagedBy&#34;, &#34;Values&#34;: [MANAGED_BY_VALUE]},
        {&#34;Name&#34;: &#34;state&#34;, &#34;Values&#34;: [&#34;available&#34;]},
    ]
    resp = ec2.describe_images(Owners=[&#34;self&#34;], Filters=filters)
    images = resp.get(&#34;Images&#34;, [])
    images.sort(key=lambda i: _parse_dt(i[&#34;CreationDate&#34;]), reverse=True)  # newest first
    return images


def _snapshots_for_ami(image: dict) -&gt; list[str]:
    snaps = []
    for bdm in image.get(&#34;BlockDeviceMappings&#34;, []):
        ebs = bdm.get(&#34;Ebs&#34;)
        if ebs and ebs.get(&#34;SnapshotId&#34;):
            snaps.append(ebs[&#34;SnapshotId&#34;])
    return snaps


def _summarise_images(images: list[dict], max_items: int = 50) -&gt; list[dict]:
    out = []
    for img in images[:max_items]:
        out.append({
            &#34;ImageId&#34;: img.get(&#34;ImageId&#34;),
            &#34;CreationDate&#34;: img.get(&#34;CreationDate&#34;),
            &#34;Name&#34;: img.get(&#34;Name&#34;),
        })
    if len(images) &gt; max_items:
        out.append({&#34;note&#34;: f&#34;truncated: showing {max_items} of {len(images)}&#34;})
    return out


def _amis_referenced_by_latest_lt_versions(launch_template_id: str, protect_versions: int) -&gt; set[str]:
    &#34;&#34;&#34;
    Protect AMIs referenced by the *latest N* launch template versions.
    &#34;&#34;&#34;
    protected = set()

    # Fetch versions (we&#39;ll sort and take the latest N)
    versions = []
    paginator = ec2.get_paginator(&#34;describe_launch_template_versions&#34;)
    for page in paginator.paginate(LaunchTemplateId=launch_template_id):
        versions.extend(page.get(&#34;LaunchTemplateVersions&#34;, []))

    if not versions:
        return protected

    # Sort by VersionNumber descending and take latest N
    versions.sort(key=lambda v: v.get(&#34;VersionNumber&#34;, 0), reverse=True)
    latest = versions[:protect_versions]

    for v in latest:
        data = v.get(&#34;LaunchTemplateData&#34;, {})
        image_id = data.get(&#34;ImageId&#34;)
        if image_id:
            protected.add(image_id)

    return protected


def lambda_handler(event, context):
    logger.info(
        &#34;Starting AMI retention. retain=%s project=%s managedby=%s dry_run=%s lt_id=%s&#34;,
        RETAIN_COUNT, PROJECT_TAG_VALUE, MANAGED_BY_VALUE, DRY_RUN, LAUNCH_TEMPLATE_ID
    )

    if RETAIN_COUNT &lt; 1:
        raise ValueError(&#34;RETAIN_COUNT must be &gt;= 1&#34;)

    images = _list_matching_amis()
    total = len(images)
    logger.info(&#34;Found %s matching AMIs.&#34;, total)
    logger.info(&#34;Matched AMIs (newest-&gt;oldest): %s&#34;, json.dumps(_summarise_images(images)))


    if total &lt;= RETAIN_COUNT:
        logger.info(&#34;&lt;= %s matching AMIs; deleting none.&#34;, RETAIN_COUNT)
        return {&#34;statusCode&#34;: 200, &#34;body&#34;: json.dumps({&#34;matching&#34;: total, &#34;deleted&#34;: 0})}

    protected = set()
    if LAUNCH_TEMPLATE_ID:
        protected = _amis_referenced_by_latest_lt_versions(LAUNCH_TEMPLATE_ID, RETAIN_COUNT)
        logger.info(&#34;Protected AMIs referenced by latest %s Launch Template versions: %s&#34;,
                RETAIN_COUNT, list(sorted(protected)))
    else:
        logger.warning(&#34;LAUNCH_TEMPLATE_ID not set; NOT protecting in-use AMIs (not recommended).&#34;)

    # Keep newest RETAIN_COUNT (always)
    keep_newest = {img[&#34;ImageId&#34;] for img in images[:RETAIN_COUNT]}

    # Eligible = older than newest RETAIN_COUNT AND not protected by LT
    older = images[RETAIN_COUNT:]
    candidates = []
    for img in older:
        ami_id = img[&#34;ImageId&#34;]
        if ami_id in protected:
            continue
        candidates.append(img)

    logger.info(&#34;Candidates for deletion (older than newest %s, not referenced by LT): %s&#34;,
                RETAIN_COUNT, len(candidates))

    deleted = []
    for img in candidates:
        ami_id = img[&#34;ImageId&#34;]
        snaps = _snapshots_for_ami(img)

        logger.info(&#34;Deleting AMI %s (snapshots=%s)&#34;, ami_id, snaps)

        if not DRY_RUN:
            ec2.deregister_image(ImageId=ami_id)
            for snap_id in snaps:
                ec2.delete_snapshot(SnapshotId=snap_id)

        deleted.append({&#34;ami&#34;: ami_id, &#34;snapshots&#34;: snaps})

    logger.info(&#34;Done. Deleted %s AMIs.&#34;, len(deleted))
    return {&#34;statusCode&#34;: 200, &#34;body&#34;: json.dumps({&#34;matching&#34;: total, &#34;deleted&#34;: len(deleted), &#34;items&#34;: deleted})}
</code></pre><p>Note: the environment vars, you can set these in terraform when you specify your <code>aws_lambda_function</code></p>
<pre tabindex="0"><code>  environment {
    variables = {
      RETAIN_COUNT       = tostring(var.ami_retain_count)
      PROJECT_TAG_VALUE  = var.project
      MANAGED_BY_VALUE   = &#34;AWSImageBuilder&#34;
      DRY_RUN            = &#34;true&#34; # flip to false when confident it&#39;s working
      LAUNCH_TEMPLATE_ID = aws_launch_template.wafr_lt.id
    }
  }
</code></pre><p>This Lambda function is, in many ways, much simpler than what I thought I’d been doing with the Image Builder Lifecycle Manager… This function:</p>
<ul>
<li>Lists AMIs by tag (Project, ManagedBy)</li>
<li>Sorts them by creation date</li>
<li>Keeps the newest N (defined by Terraform variable <code>ami_retain_count</code>)</li>
<li>Deregisters anything older</li>
<li>Deletes the snapshots referenced by those AMIs</li>
<li>Never deletes AMIs currently referenced by the latest N Launch Template (also defined by <code>ami_retain_count</code>.)</li>
</ul>
<p>This runs on a schedule I control. And understand!<br>
It&rsquo;s observable, repeatable, and it can be run in dry-run mode while testing.</p>
<p>Most importantly, the behaviour can be explained in one sentence:</p>
<blockquote>
<p>“We keep the 5 newest AMIs for this project, and we never delete one that’s in use.”</p>
</blockquote>
<p>That is the rule I <em>thought</em> I was configuring when I attempted to enable lifecycle retention — but now it’s actually true!</p>
<h2 id="why-a-custom-solution-won--why-im-okay-with-that">Why a custom solution won (&amp; why I&rsquo;m okay with that)</h2>
<p>After working through this in detail, I came to a conclusion that surprised me:<br>
<strong>Image Builder lifecycle policies are not a good fit for intuitive AMI retention.</strong></p>
<p>They are:</p>
<ul>
<li>metadata-centric</li>
<li>recipe-scoped</li>
<li>on their own schedule</li>
<li>capable of deleting artifacts without leaving usable AMIs behind (talk about sharp edges!)</li>
</ul>
<p>This doesn’t mean lifecycle policies are broken — but it does make them dangerous if your mental model is <strong>“Keep the last <em>N</em> usable AMIs.”</strong></p>
<p>In my experience this model doesn’t map cleanly to how Image Builder lifecycle works.</p>
<p>By contrast my Lambda function is:</p>
<ul>
<li>explicit - I could set my own boundaries that did what I expected them to do.</li>
<li>controlled - it runs when <em>i</em> want.</li>
<li>able to add clear and simple guardrails - e.g. <em>don&rsquo;t delete any AMIs still in use, regardless of other filters</em>.</li>
<li>Auditable - see what&rsquo;s going on easily in the execution logs.</li>
<li>Test friendly. Flip &ldquo;dry-run&rdquo; to &rsquo;true&rsquo; to test what would be deleted before you take the training wheels off.</li>
</ul>
<p>The result was a solution that worked as I expected, trimmed my AMIs and Snapshots to the number I wanted, and stopped my bill from growing forever with the ghost of a million unused snapshots, allowed me to safely test changes <em>when I wanted</em>. And it only cost only a tiny amount in Lambda usage. Worth it.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>EC2 Image Builder is excellent at building &amp; customising images.<br>
EventBridge and Lambda are excellent at reacting to them.</p>
<p>But AMI retention is an operational concern, not a build concern — and once I treated it that way, the solution became simpler, safer, and easier to understand.</p>
<p>If you’re currently using Image Builder lifecycle today and it works for you, that’s great.</p>
<p>If, however, your mental model is <em>“keep the last N usable AMIs”</em>, it’s worth pausing and double-checking that the system is actually doing what you think it is.</p>
<p>Sometimes the most robust solution is the one you can explain easily — and without waiting 24 hours to see what it decided to do.</p>
<h2 id="additional-resources">Additional Resources</h2>
<ul>
<li><a href="https://github.com/jmcmillan1873/autoscaling-with-imagebuilder">Complete EC2 Image Builder with Lifecycle Management Example</a></li>
<li><a href="https://aws.amazon.com/image-builder/">Amazon EC2 Image Builder</a></li>
<li><a href="https://aws.amazon.com/pm/lambda/">AWS Lambda</a></li>
<li><a href="https://aws.mcmillearn.net/posts/half-baked-images/">Half Baked Images - previous blog post</a></li>
</ul>
<hr>
<p><em>This post is part of my AWS technical blog series. Found this helpful? Connect with me on <a href="https://www.linkedin.com/in/john-mcmillan-aaa4274/">LinkedIn</a> or check back here for more AWS content and discussions.</em></p>
]]></content:encoded><category>AWS</category><category>EC2 Image Builder</category><category>Lambda</category><category>AWS</category><category>EC2</category><category>Auto Scaling Group</category><category>EC2 Image Builder</category><category>AWS EC2 Image Builder</category><category>AWS Auto Scaling Group</category><category>AWS EventBridge</category><category>AWS Lambda</category><enclosure url="https://aws.mcmillearn.net/images/Half-baked-lifecycles.png" type="image/png"/></item><item><title>Half baked Images?</title><link>https://aws.mcmillearn.net/posts/half-baked-images/</link><pubDate>Thu, 02 Oct 2025 09:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/posts/half-baked-images/</guid><description>Making ImageBuilder work with Terraform managed Auto scaling</description><content:encoded><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>Using Terraform to deploy and manage an Auto Scaling Group that utilises custom AMIs built by EC2 Image Builder and dealing with the limitations of EC2 Image Builder&rsquo;s native capabilities for managing Launch Templates.</p>
<h1 id="what-were-we-asked">What were we asked?</h1>
<p>I&rsquo;ve recently worked on a project that required an Auto Scaling Group of EC2 instances that utilised custom-built AMIs produced by EC2 Image Builder. The requirement was to have an EC2 Image Builder pipeline produce weekly images based on Amazon Linux 2023, with various custom components applied.<br>
It&rsquo;s quite straightforward to do this in Terraform, so happy days!</p>
<p>If you&rsquo;ve used Terraform to manage Auto Scaling Groups in AWS, you&rsquo;ll know that this is relatively  straightforward too. There are lots of examples and use cases out there to draw from.<br>
So it should be super simple to marry those two concepts together into one simple, happy, solution you&rsquo;d think.<br>
Yeah right!</p>
<p>If you want to skip the dramatic retelling of my pain and just get the answer, skip to <a href="#so-what-was-the-solution">the solution</a></p>
<h2 id="whats-the-problem-go">What&rsquo;s the problem? Go!</h2>
<p>The first two parts <em>are</em> straightforward - the ASG definition and EC2 Image Builder pipeline setup in Terraform. As is the rest of the Terraform really. Working with my buddy we quickly had IaC that would deploy the VPC that would host our Auto Scaling Group, the Launch Template that would complement that ASG, and the EC2 Image Builder components, recipe, pipeline, and distribution, etc. As well as all the trimmings, IAM policies, security groups, KMS keys, etc, etc.<br>
During the initial deploy attempt we encountered our first red flag. We had valid Terraform code, but it threw up this error:</p>
<pre tabindex="0"><code>Planning failed. Terraform encountered an error while generating this plan.

╷
│ Error: Your query returned no results. Please change your search criteria and try again.
│
│   with data.aws_ami.latest_custom,
│   on data.tf line 486, in data &#34;aws_ami&#34; &#34;latest_custom&#34;:
│  486: data &#34;aws_ami&#34; &#34;latest_custom&#34; {
│
</code></pre><p>I&rsquo;d used a data source to capture the ID of the latest image that EC2 Image Builder would produce, but of course that hadn&rsquo;t run yet.<br>
I thought I&rsquo;d been smart, I had foreseen the bootstrapping issue, and thought I&rsquo;d get around it by temporarily pointing the image_id at an AWS managed SSM parameter that stores the Amazon Linux 2023 AMI ID.</p>
<pre tabindex="0"><code>resource &#34;aws_launch_template&#34; &#34;my-cool-project-resource&#34; {
  name_prefix            = &#34;my-cool-project-name-lt-&#34;
  #image_id               = data.aws_ami.latest_custom.id
  image_id               = data.aws_ssm_parameter.al2023.id
</code></pre><p>This didn&rsquo;t prevent the error above. To my shame (not really, I&rsquo;m actually quite pragmatic) I commented out the data source for my latest custom image and was able to successfully deploy my infrastructure. I immediately reversed the commented out sections. <code>data.aws_ami.latest_custom</code> was reinstated, and the Launch Template was reverted back to <code>image_id = data.aws_ami.latest_custom.id</code> and all worked well. Sorted.</p>
<h3 id="5-influence-points-if-you-see-where-this-is-going">+5 influence points if you see where this is going</h3>
<p>It wasn&rsquo;t sorted. Not completely.<br>
I could now successfully deploy the infrastructure reliably, I&rsquo;d hacked my way past the bootstrapping issue, but comforted by my solemn vow to myself that I&rsquo;d go back and fix that. Scout&rsquo;s honour.<br>
<img alt="IdontBelieveYou" loading="lazy" src="/posts/half-baked-images/dontbelieveyou.gif"></p>
<p>The situation was better but not quite what we needed. We had an EC2 Image Builder pipeline that ran to a schedule and worked as expected. Our Launch Template looked good but it wasn&rsquo;t using the most recent image. It was using the most recent -1.  Consider this sequence of events:</p>
<ol>
<li>Terraform deploys IaC, creating ASG, Launch Template, and EC2 Image builder pipeline.</li>
<li>EC2 Image Builder was triggered manually to confirm it was working.</li>
<li>EC2 Image Builder produces a new version of the AMI.</li>
<li>Launch Template is still running the TF deployed AMI version and doesn&rsquo;t know about the one EC2 Image Builder produced.</li>
</ol>
<p>This surfaced what might have been obvious to you dear reader, that the Launch Template <strong>must</strong> be built before we&rsquo;ve built any images.<br>
Also that our Pipeline would run independently of our Terraform code - and so our Launch Template would never know about the latest image until we next ran Terraform to update the Launch Template. That&rsquo;s not ideal as the intention was to set up a CI/CD pipeline to run the Terraform code upon merges to the main branch.</p>
<p>I was opposed to the idea of scheduling the Terraform code to run <em>after</em> the pipeline schedule. That wouldn&rsquo;t be pragmatic, that would be messy.</p>
<h3 id="why-dont-you-just-update-the-lt-natively-from-ec2-image-builder">Why don&rsquo;t you just update the LT natively from EC2 Image Builder?</h3>
<p>Reasons!<br>
Mostly that I didn&rsquo;t know that was a thing at this point. I started looking at how other people have solved this, because surely using custom-built AMIs in Auto Scaling Groups is super common? It kinda is, but so is this challenge.</p>
<p>In actual fact, the distribution settings within EC2 Image Builder allow for updating a Launch Template with the latest AMI&hellip; directly. It&rsquo;s explained nicely <a href="https://docs.aws.amazon.com/imagebuilder/latest/userguide/dist-using-launch-template.html">here</a>.</p>
<p>So that resulted in me adding this <code>launch_template_configuration</code> block to my distribution config:</p>
<pre tabindex="0"><code>resource &#34;aws_imagebuilder_distribution_configuration&#34; &#34;dist&#34; {
  name = &#34;${var.project}-dist&#34;

  distribution {
    region = var.region

    ami_distribution_configuration {
      name        = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      description = &#34;A descriptive description describing things&#34;

      ami_tags = {
        OS       = &#34;AmazonLinux2023&#34;
        Hardened = &#34;true&#34;
        Name     = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      }
    }
    launch_template_configuration {
      launch_template_id = aws_launch_template.my-cool-project-resource.id
      account_id         = local.account_id
    }
  }
}
</code></pre><p>My distribution would now update the Launch Templates with details of the new AMI ID.<br>
And it did.</p>
<h3 id="half-baked-images">Half Baked Images!</h3>
<p>This was a step forward, if you&rsquo;d first pivoted 180 degrees.<br>
While it did solve the problem of the Launch Template not having the latest AMI - it absolutely <em>did</em> have the latest AMI now - it came at a price.</p>
<p>So, for some reason the documentation gods appear not to have fully explained (please tell me on Linkedin if I&rsquo;m wrong) EC2 Image Builder Distribution Configuration can only configure <em>some</em> elements of the Launch Template, not all!</p>
<p>And it doesn&rsquo;t update the Launch Template in the sense that it retains previous config and simply updates the AMI ID. It updates in the sense that it creates a <strong>new</strong> version with the details you&rsquo;ve told it to. In my case the Account ID and the Launch Template to update with the AMI ID. That makes sense when you step back and think about it, but it also felt unintuitive.</p>
<h3 id="why-is-this-bad">Why is this bad?</h3>
<p>Because the updated version of the Launch Template no longer contains several key details about the rest of the required configuration, Instance profile, Security groups, key pair, etc.</p>
<p>I&rsquo;ll save you the suspense - I checked with Amazon Q to point me to the explanation, and it had this to say:</p>
<blockquote>
<p><em>&ldquo;Unfortunately, EC2 Image Builder cannot preserve existing Launch Template settings when updating the AMI. When EC2 Image Builder creates a new Launch Template version through its distribution settings, it only copies the AMI ID to the new version - it doesn&rsquo;t inherit or preserve other configuration settings like instance profiles, security groups, or other parameters from your existing Launch Template&hellip;<br>
&hellip;the limitations are not explicitly stated in the official AWS docs. The documentation focuses on how to configure EC2 Image Builder to update Launch Templates, but it doesn&rsquo;t clearly specify what happens to existing Launch Template settings when it creates a new version.&rdquo;</em></p>
</blockquote>
<p>The result of this was that my ASG was now spinning up instances with no instance profile, which wasn&rsquo;t the only misconfiguration introduced, but this alone was a show stopper.</p>
<h2 id="so-what-was-the-solution">So what was the solution?</h2>
<p>The solution I settled on was a two-parter, but let&rsquo;s start by recapping what we want to happen:</p>
<ol>
<li>Using Terraform: deploy &amp; manage infrastructure that will create an ASG, Launch Templates, and an EC2 Image Builder pipeline that will update images frequently.</li>
<li>Shared custody of the latest AMI ID &amp; Launch Template configuration between EC2 Image Builder and Terraform. By that I mean:<br>
a. Terraform creates the infrastructure that holds the AMI details and doesn&rsquo;t complain when&hellip;<br>
b. The Launch Template is always referring to the latest AMI ID.</li>
<li>The Launch Template is updated as soon as an updated image is produced, without waiting for Terraform.</li>
<li>Terraform won&rsquo;t freak out when it realises the Launch Template is using a newer AMI ID than it was expecting.</li>
</ol>
<p>So practically how do we do this?</p>
<h3 id="step-1---get-the-iac-correct">Step 1 - Get the IaC correct</h3>
<p>First of all, Terraform needs to create an SSM Parameter as shown in this example:</p>
<pre tabindex="0"><code>
data &#34;aws_ssm_parameter&#34; &#34;al2023&#34; {
  name = &#34;/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64&#34;
}

resource &#34;aws_ssm_parameter&#34; &#34;custom_built_image_id&#34; {
  name        = &#34;/imagebuilder/${var.project}/custom_id&#34;
  description = &#34;SSM Parameter for storing the AMI ID of the image built from EC2 Image Builder&#34;
  type        = &#34;String&#34;
  data_type   = &#34;aws:ec2:image&#34;
  value       = data.aws_ssm_parameter.al2023.value

  lifecycle {
    ignore_changes = [
      value
    ]
  }
}
</code></pre><p>So what did we accomplish with the SSM parameter definition in Terraform?<br>
This is our source of truth for our AMI image.</p>
<ul>
<li>The Terraform definition for the Launch Template should retrieve the value stored here. Hence why it needs to be seeded with a real AMI ID to start with.</li>
<li>Something (no spoilers yet) needs to update the Launch Template with the latest custom built AMI ID. Hence why we have the lifecycle policy ignoring changes to the value.</li>
<li><strong>Important 1</strong> - The managed service role for EC2 Image Builder only has permissions to publish to items in Parameter Store that begin with the prefix <code>/imagebuilder/</code> - see <a href="https://docs.aws.amazon.com/imagebuilder/latest/userguide/tutorial-ssm-parameters-recipe.html">AWS docs on that point</a>. i.e. You <em>must</em> use a <code>name</code> similar to the one in the example above.</li>
<li><strong>Important 2</strong> - <code> data_type   = &quot;aws:ec2:image&quot;</code> <strong>must</strong> be set otherwise the Distribution Configuration in EC2 Image Builder will fail to update the parameter. (Ask me how I know this!)</li>
<li><strong>Important 3</strong> - Don&rsquo;t forget to ignore changes to the <code>value</code> as shown above!</li>
</ul>
<p>Now that we have somewhere to store the AMI ID, we need to make sure its referred to in our code, specifically in the Launch Template and the EC2 Image Builder distribution configs:</p>
<pre tabindex="0"><code>resource &#34;aws_launch_template&#34; &#34;my_lt&#34; {
  name_prefix            = &#34;my-lt-&#34;
  image_id               = aws_ssm_parameter.custom_built_image_id.value
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.private_sg.id]

  iam_instance_profile {
    name = aws_iam_instance_profile.custom_permissions.name
  }

  block_device_mappings {
    device_name = &#34;/dev/xvda&#34;
    ebs {
      volume_size = 20
      volume_type = &#34;gp3&#34;
      encrypted   = true
    }
  }

  tag_specifications {
    resource_type = &#34;instance&#34;
    tags = {
      Name    = &#34;NamingIsHard&#34;
      Project = var.project
    }
  }
}

resource &#34;aws_imagebuilder_distribution_configuration&#34; &#34;dist&#34; {
  name = &#34;${var.project}-dist&#34;

  distribution {
    region = var.region

    ami_distribution_configuration {
      name        = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      description = &#34;Custom built AL2023 image&#34;

      ami_tags = {
        OS       = &#34;AmazonLinux2023&#34;
        Hardened = &#34;true&#34;
        Name     = &#34;${var.project}-{{imagebuilder:buildDate}}&#34;
      }
    }

    ssm_parameter_configuration {
      parameter_name = aws_ssm_parameter.custom_built_image_id.name
      ami_account_id = local.account_id
    }
  }
}
</code></pre><p>So now the Launch Template is happy, because even at boot time we&rsquo;ve given it a valid AMI ID.<br>
EC2 Image Builder is no longer sabotaging our Launch Template and is instead updating the SSM Parameter Store.</p>
<h3 id="step-2---join-the-dots">Step 2 - Join the dots</h3>
<p>So, thus far we&rsquo;ve completed our deployment, but we still need something to handle the Launch Template updates. Cue <strong>EventBridge</strong> &amp; <strong>Lambda</strong>.<br>
Whereas the native option in EC2 Image Builder would allow us to create a new version of the Launch Template with the new AMI ID, it didn&rsquo;t allow us to specify the other important requirements like security group, profile etc.<br>
A custom Lambda function allows us to do exactly that.</p>
<p>e.g. We can take this function, deploy it using Terraform&hellip;</p>
<pre tabindex="0"><code>import boto3
import os

ssm = boto3.client(&#34;ssm&#34;)
ec2 = boto3.client(&#34;ec2&#34;)

PARAM_NAME = os.environ[&#34;SSM_PARAM_NAME&#34;]
LT_ID = os.environ[&#34;LAUNCH_TEMPLATE_ID&#34;]
INSTANCE_TYPE = os.environ[&#34;INSTANCE_TYPE&#34;]
SECURITY_GROUP_ID = os.environ[&#34;SECURITY_GROUP_ID&#34;]
IAM_INSTANCE_PROFILE = os.environ[&#34;IAM_INSTANCE_PROFILE&#34;]

def handler(event, context):
    # Get latest AMI from SSM
    response = ssm.get_parameter(Name=PARAM_NAME)
    ami_id = response[&#34;Parameter&#34;][&#34;Value&#34;]

    # Create new Launch Template version
    ec2.create_launch_template_version(
        LaunchTemplateId=LT_ID,
        SourceVersion=&#34;$Default&#34;,
        LaunchTemplateData={
            &#34;ImageId&#34;: ami_id,
            &#34;InstanceType&#34;: INSTANCE_TYPE,
            &#34;SecurityGroupIds&#34;: [SECURITY_GROUP_ID],
            &#34;IamInstanceProfile&#34;: {&#34;Name&#34;: IAM_INSTANCE_PROFILE}
        }
    )

    # Set the new version as default
    latest = ec2.describe_launch_template_versions(
        LaunchTemplateId=LT_ID,
        Versions=[&#34;$Latest&#34;]
    )[&#34;LaunchTemplateVersions&#34;][0][&#34;VersionNumber&#34;]

    ec2.modify_launch_template(
        LaunchTemplateId=LT_ID,
        DefaultVersion=str(latest)
    )

    print(f&#34;Updated LT {LT_ID} to use {ami_id}&#34;)
    return {&#34;statusCode&#34;: 200, &#34;ami_id&#34;: ami_id}
</code></pre><h4 id="what-will-this-function-do">What will this function do?</h4>
<p>It takes five environment variables that are used to ensure all the required data for creating a <em>useful</em> new version of the Launch Template is included, but simply put it&rsquo;s doing three things:</p>
<ul>
<li>It obtains the latest AMI ID - that EC2 Image Builder&rsquo;s Distribution config applied to the SSM Parameter.</li>
<li>Create a new version of the Launch Template, passing in the required details, image ID, instance type, security group(s), &amp; Instance profile for permissions.</li>
<li>It updates the Launch Template to set the latest version it just created as the default version.</li>
</ul>
<h4 id="how-do-we-trigger-that-function">How do we trigger that function?</h4>
<p>Now that we have the function to handle the updates, we just need to trigger it appropriately. We do that using EventBridge, and we can also deploy this from Terraform using something like this:</p>
<pre tabindex="0"><code># This is used to trigger Lambda to update the ASG Launch Template when there&#39;s a new AMI available.

resource &#34;aws_cloudwatch_event_rule&#34; &#34;imagebuilder_completed&#34; {
  name          = &#34;${var.project}-imagebuilder-completed&#34;
  description   = &#34;Trigger on successful image builds&#34;
  event_pattern = &lt;&lt;EOF
{
  &#34;source&#34;: [&#34;aws.imagebuilder&#34;],
  &#34;detail-type&#34;: [&#34;EC2 Image Builder Image State Change&#34;],
  &#34;detail&#34;: {
    &#34;state&#34;: {
      &#34;status&#34;: [&#34;AVAILABLE&#34;]
    }
  }
}
EOF
}

resource &#34;aws_cloudwatch_event_target&#34; &#34;trigger_lambda&#34; {
  rule      = aws_cloudwatch_event_rule.imagebuilder_completed.name
  target_id = &#34;LaunchTemplateUpdater&#34;
  arn       = aws_lambda_function.update_launch_template.arn
}
</code></pre><p>This creates an EventBridge rule that is triggered every time your EC2 Image Builder pipeline successfully produces an AMI with a status of &lsquo;AVAILABLE&rsquo;.</p>
<p>That rule in turn triggers the Lambda function shown above.
So now we&rsquo;ll have this sequence:</p>
<ol>
<li>EC2 Image Builder runs and produces a new Image</li>
<li>It also updates the SSM Parameter store with the latest AMI ID</li>
<li>When the new AMI is updated to be &lsquo;AVAILABLE&rsquo;, our EventBridge Rule is triggered.</li>
<li>The EventBridge rule in turn runs the Lambda function to retrieve this new ID and create a new Launch Template version.</li>
</ol>
<p>The result is that when the ASG next scales up, it&rsquo;ll use the latest AMI ID.<br>
It also means that on the next Terraform run it&rsquo;ll pull the latest AMI ID from SSM and recognise that no change is required.</p>
<h2 id="conclusion">Conclusion</h2>
<p>If you want to use EC2 Image Builder to manage AMIs for an ASG, all but the most simple use cases will require some extra plumbing when it comes to handling the Launch Template configuration.<br>
In those more advanced scenarios a method as highlighted here will be effective in handling the synchronisation between Terraform&rsquo;s view of the world and ensuring the Launch Template is always up to date (with a useful Instance Profile.)</p>
<h2 id="additional-resources">Additional Resources</h2>
<ul>
<li><a href="https://github.com/jmcmillan1873/autoscaling-with-imagebuilder">Complete Auto Scaling Group with EC2 Image Builder Example</a></li>
<li><a href="https://docs.aws.amazon.com/imagebuilder/latest/userguide/tutorial-ssm-parameters-recipe.html">EC2 Image Builder &amp; SSM</a></li>
</ul>
<hr>
<p><em>This post is part of my AWS technical blog series. Found this helpful? Connect with me on <a href="https://www.linkedin.com/in/john-mcmillan-aaa4274/">LinkedIn</a> or check back here for more AWS content and discussions.</em></p>
]]></content:encoded><category>AWS</category><category>EC2 Image Builder</category><category>Auto Scaling Group</category><category>AWS</category><category>EC2</category><category>Auto Scaling Group</category><category>EC2 Image Builder</category><category>AWS EC2 Image Builder</category><category>AWS Auto Scaling Group</category><category>AWS EventBridge</category><category>AWS Lambda</category><enclosure url="https://aws.mcmillearn.net/images/half-baked-title.png" type="image/png"/></item><item><title>What is Kiro and Spec Driven Development?</title><link>https://aws.mcmillearn.net/posts/kiro-and-sdd/</link><pubDate>Tue, 02 Sep 2025 09:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/posts/kiro-and-sdd/</guid><description>Building an AWS blogging platform with Kiro</description><content:encoded><![CDATA[<h1 id="from-inspiration-to-action-with-ai">From inspiration to action. With AI.</h1>
<p>Inspired by the talented <a href="https://www.cloudypandas.ch/">Chris Bingham</a>, an <a href="https://aws.amazon.com/partners/ambassadors/">AWS Ambassador</a> here at Fujitsu, I recently set myself a new target: to apply for the Ambassador program myself. Chris spoke positively about the program, and it struck me as the kind of nudge I needed to step outside my comfort zone. It also felt like an opportunity to help bring positive attention to the good AWS work being done at Fujitsu.</p>
<p>However, there’s one glaring weakness with any application I might submit: I&rsquo;ve never really shared knowledge publicly. So, I decided to start there. Build a habit of sharing knowledge regularly and then decide later whether I want to keep it up long-term and actually apply.</p>
<p>The next question to answer was, <em>how would I share articles and opinions?</em><br>
Enter <a href="https://Kiro.dev/">Kiro</a>.</p>
<h2 id="what-is-kiro">What is Kiro?</h2>
<p>Kiro is a new tool from AWS that promotes Spec Driven Development. It&rsquo;s a forked version of Visual Studio Code, so it&rsquo;s familiar and supports the same extensions.</p>
<p>The Spec driven Development approach that it encourages guides the user to plan out a project, quickly but thoroughly, before generating code. That’s a little different from some of the other AI-assisted coding tools out there that focus purely on code generation.</p>
<p>I managed to get onto the recent preview of Kiro — and so I wanted to use it in a way that would let me experience it fully.</p>
<p>I decided to use Kiro to plan, design, build, and deploy a blogging site to host my articles - this website.
Well three out of four isn&rsquo;t bad. &lsquo;Deploy&rsquo; was achieved outside of Kiro, but I&rsquo;ll explain that later.</p>
<h3 id="what-is-spec-driven-development">What is Spec Driven development?</h3>
<p>Spec Driven development (SDD) is a methodology that encourages the developer to use a coding assistant in the &lsquo;right&rsquo; way. You start with a &lsquo;specifcation&rsquo;. Define what you want to achieve, inputs, outputs, etc.</p>
<p>SDD doesn&rsquo;t immediately launch into generating code. It uses your prompt to build the spec. There are three main phases to the process that we&rsquo;ll look at:</p>
<ol>
<li>Create requirements - generate a  <strong>well written project plan</strong></li>
<li>Create the design - generate a <strong>design capable of meeting the plan</strong></li>
<li>Create the tasks - generate the <strong>tasks capable of delivering</strong> your project</li>
</ol>
<h4 id="start-with-a-prompt">Start with a prompt:</h4>
<p>The entirety of my prompt to Kiro was:</p>
<p><img alt="Kiro, make me a blog." loading="lazy" src="/posts/kiro-and-sdd/blog-prompt.png"></p>
<h4 id="create-the-requirements">Create the requirements</h4>
<p>As you can see, Kiro was happy to oblige and generated <code>requirements.md</code>, a list of high level requirements for my blog, written in the form of user stories with acceptance criteria.<br>
For example:</p>
<div class="highlight"><div style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>### Requirement 1
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>**User Story:** As an aspiring AWS Ambassador, I want to host my own blogging platform on AWS, so that I can publish technical articles and demonstrate my AWS expertise.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>#### Acceptance Criteria
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#cf222e">1.</span> WHEN I access the blogging platform THEN the system SHALL be hosted entirely on AWS infrastructure
</span></span><span style="display:flex;"><span><span style="color:#cf222e">2.</span> WHEN I create a blog post THEN the system SHALL support rich text editing with code syntax highlighting
</span></span><span style="display:flex;"><span><span style="color:#cf222e">3.</span> WHEN I publish an article THEN the system SHALL make it publicly accessible with SEO optimization
</span></span><span style="display:flex;"><span><span style="color:#cf222e">4.</span> WHEN visitors access my blog THEN the system SHALL provide fast loading times and high availability
</span></span></code></pre></td></tr></table>
</div>
</div><p>In total, 5 requirements were created from my prompt. I changed only one. A line in one of the acceptance criteria from <em>&ldquo;generate CloudFormation or Terraform IAC&rdquo;</em>, to <em>&ldquo;generate Terraform IAC&rdquo;</em>.</p>
<p>Now, have a quick look at my original prompt again and consider this:<br>
I received five, well written, comprehensive requirements from <em>that</em> prompt. There’s real value in going from a rough idea to a clear, and easily edited, list of requirements – something that gives ‘shape’ to the idea. It really accelerates the process of going from idea -&gt; to POC -&gt; to market.</p>
<h4 id="create-the-design">Create the design</h4>
<p>Kiro waited patiently for me to review the requirements and make any amendments I wanted. When I was happy with the requirements, Kiro then produced <code>design.md</code>. This is a proposed solution, including a summary of that solution, high level archtitecture, list of components, and other relevant details to my plan. Here&rsquo;s a snippet of my design:</p>
<div class="highlight"><div style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="background-color:#f7f7f7;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-markdown" data-lang="markdown"><span style="display:flex;"><span>## Overview
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>The AWS Blogging Platform is designed as a cost-effective, Hugo-based static site solution that demonstrates AWS best practices while providing excellent performance and SEO capabilities. The platform leverages Hugo static site generation with AWS hosting services to create an extremely affordable, fast, and secure blogging solution suitable for technical content creation and AWS expertise demonstration for Ambassador applications.
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>## Architecture
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>### Primary Architecture: Hugo Static Site Generator
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>**Selected Architecture**: Hugo-based static site with AWS hosting
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **Frontend**: Hugo-generated static site on S3 + CloudFront
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **Content**: Markdown files in Git repository
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **CMS**: Git-based workflow with GitHub repository
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **CI/CD**: GitHub Actions for automated builds and deployment
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **Cost**: ~$1-5/month for moderate traffic
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **Pros**: Extremely fast, cheapest option, Git-based content management, excellent SEO, perfect for technical blogging
</span></span><span style="display:flex;"><span><span style="color:#cf222e">-</span> **Cons**: No dynamic features, requires Git knowledge, limited admin interface
</span></span></code></pre></td></tr></table>
</div>
</div><p>In actual fact Kiro created three high level proposals:</p>
<ol>
<li>Servers Static site using React/Node.js hosted on S3 + CloudFront, and using DynamoDB.</li>
<li>Container based solution - React app on ECS Fargate + RDS PostgreSQL</li>
<li>A Hybrid approach which didn&rsquo;t fully make sense using API Gateway, Lambda, ECS, and RDS.</li>
</ol>
<p>After some cherry picking on my part, I chose a design close to option 1 but dropped React/Node.js and DyanmoDB, and instead used <a href="https://gohugo.io/">Hugo</a> to convert markdown to static HTML hosted on S3 + CloudFront.</p>
<p>During the review of design.md I spotted the only misunderstanding I encountered with Kiro. My prompt included: <em>“I’d like to review architectural diagrams and assess their suitability”</em> .</p>
<p>Kiro interpreted this as a request for my blog site to also have functionality and capability to receive architectural diagrams. I realised that when I spotted this in the original <code>design.md</code>:</p>
<pre tabindex="0"><code>### Architecture Review System
┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│  Architecture   │────│   Diagram        │────│   Cost          │
│  Generator      │    │   Renderer       │    │   Calculator    │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
         │             ┌──────────────────┐    ┌─────────────────┐
         └─────────────│   Template       │────│   Validation    │
                       │   Library        │    │   Engine        │
                       └──────────────────┘    └─────────────────┘
</code></pre><p>That’s a lesson for me as a user of the tool about being clear and unambiguous. It’s also a reminder that you do <em>actually</em> need to review the requirements, designs, and tasks that it produces.</p>
<p>Pretty cool though! – I’ve added the idea to my backlog.</p>
<h4 id="create-the-tasks">Create the tasks</h4>
<p>As before, Kiro will wait for you to review, amend, and finalise the design before proceeding to the next step. That could be by either manually updating <code>design.md</code> yourself, or interacting with Kiro to describe the changes you want, and Kiro will update the design.</p>
<p>Once you&rsquo;ve settled on the requirements, then the design, Kiro will generate <code>tasks.md</code>. This is a list of tasks that are all directly a result of, and therefore, linked back to, the requirements, for example:</p>
<p><img alt="Kiro, make me tasks" loading="lazy" src="/posts/kiro-and-sdd/blog-tasks.png"></p>
<p>These tasks are &lsquo;clickable&rsquo;. Where you now see <code>Task completed</code>, it orginially read <code>start task</code>. Clicking that was all that was required to complete the task(s). I had the choice of clicking <code>2. create aws infrastructure with Terraform</code> to complete all of the tasks in step 2, or I could execute each one individually and review as I went.<br>
I was moving quickly but also had short pauses to review what had been achieved in bite size chunks.</p>
<p>It felt fast <em>and</em> considered. What do I mean by that?</p>
<p>Having reviewed <code>tasks.md</code> and about to generate the code - I was less than 2 hours into the process. i.e. 2 hours prior I hadn’t submitted my request to Kiro.</p>
<p>Now I was generating complex code that was tuned to specific requirements and validated tasks – that’s undeniably fast. However, in that time I’d still had an opportunity to review the requirements and the subsequent designs, and to make changes. Therefore, the code I was about to create had been carefully considered.</p>
<h3 id="does-sdd-differ-from-vibe-coding">Does SDD differ from Vibe coding?</h3>
<p>Yes.</p>
<p><strong>Vibe coding</strong> is fun. When done right, it’s a great way to experiment or knock out a quick fix. But it doesn’t ask you to stop and think about what your requirements actually are.<br>
If you get distracted, you can easily lose your place. The “plan” (if you had one) lives only in your head, which makes it hard to share with others, human or AI. And if you don’t have a plan, how exactly do you measure progress, or  build in testing?</p>
<p><strong>Spec Driven Development (SDD)</strong> flips that around.<br>
The small upfront investment of writing down your requirements pays off quickly. You’re forced to clarify your goals and capture them in a way that’s easy to edit later. Kiro can then update the design and tasks automatically. Your spec becomes a single source of truth that’s easy to share with colleagues, clients, or AI helpers. Testing stops being an afterthought, and becomes super simple to embed into the process from the start because your objectives are clear.</p>
<h3 id="why-does-that-matter">Why does that matter?</h3>
<p>Bluntly - it offers significant time savings. Using Kiro and SDD I estimate that I saved at least 3 weeks on my project.</p>
<p>Consider this scenario. An account manager receives an opportunity to create a small static marketing website for a client wishing to reach customers with updates, announcements, and new offerings. The subsequent timeline might look like this:</p>
<ul>
<li>The account manager schedules a call with the project team (A project manager, solutions architect, and a couple of cloud engineers). Availability of the team means it&rsquo;s two days before the call happens. (+2 days)</li>
<li>After the call, the solution architect is tasked with documenting the requirements and circulating for review, correction, and agreement. (+1 day)</li>
<li>The solution architect takes a day to generate a high level design (+1 day)</li>
<li>Feedback from the team results in iteration over the next couple of days (+2 days)</li>
<li>Once finalised, one of the cloud engineers works with the PM to create user stories in Jira. (+1 day)</li>
<li>The cloud engineers begin the process of writing code in terrform to define the infrastructure hosting the environment. (+5 to 8 days)</li>
<li>Deploying the infrastructure reveals some bugs and improvement opportunities (+2 days)</li>
<li>The cloud engineers switch to generate the CICD pipelines in Github actions to test, build, and deploy the site content. (+5 days)</li>
<li>Oops - that thing we forgot to account for (+ 1 day)</li>
</ul>
<p>Does that sound familiar?<br>
Thats roughly 18-21 working days involving 4-5 people. That&rsquo;s approximately four working weeks. And of course in the real world that team won&rsquo;t get four weeks uninterrupted to focus on that project - holidays, sickness, other priorities jumping the queue, etc. There will be additional delays, and the longer something runs, the more it&rsquo;s affected by these sorts of delays and interruptions.</p>
<h2 id="outcomes-and-timeline">Outcomes and timeline</h2>
<p>Those steps I described above are equivalent to what I undertook to create this blogging site, but it took me just <strong>4 days to get the site up and running</strong>.</p>
<p>Approximately 3 days of that was spent fine tuning, tweaking, corrections, etc - but it took just <strong>3 hours</strong> to achieve the following in Kiro:</p>
<ul>
<li>Confirm the project (my) requirements  - <code>requirements.md</code></li>
<li>Plan the project, outline the steps involved</li>
<li>Generate and propose four high level solutions with comparative pros and cons on cost and complexity</li>
<li>Select one of the four options, iterate that design to get to a solution I was happy with</li>
<li>Produce the high level design architecture for the chosen solution - <code>design.md</code></li>
<li>Define all of the steps needed to make the project a reality - <code>tasks.md</code></li>
<li>Generate the CICD pipeline workflows in GitHub Actions, to validate PR&rsquo;s, perform build and integration tests, and deploy content to the solution.</li>
<li>Generate the first draft of Terraform IAC including, S3, Cloudfront, OAC, SSL certs via ACM, IAM Roles + least privilege policies, CloudWatch, Budgets, Route 53, Notification integrations with Slack, etc.</li>
<li>S3 bucket for content and state file backups.</li>
<li>Create 2 repositories, one for the IAC, one for blogging content. Both with specific security scanning, pre commit hooks etc.</li>
</ul>
<p><strong>3 hours effort</strong> to do <em>that</em>? - It blew my mind.</p>
<p>Now as I&rsquo;ve already said, I wasn&rsquo;t finished in 3 hours, it was more like 4 days.<br>
Having got to the first draft of Terraform code, I wanted to make some changes, validate what had been produced, customise to suit my needs, etc. I also had some corrections and customisations to make on the GitHub actions workflows.</p>
<p>That process took me a further 3 working days (approx. 20hrs effort) to deploy, fix, tweak, and iterate to get the platform working as you see it now.</p>
<p>So less than 5 days, for a solo experienced cloud architect/engineer to complete something that was ready for deploying and acceptance testing.<br>
Realistically that traditionally might take a team 3-4 weeks.</p>
<h2 id="what-did-i-learn-from-this">What did I learn from this?</h2>
<p>AI powered coding tools can accelerate individual tasks. e.g. writing scripts, or a Terraform module. These tasks could be chained to accelerate aspects of a project delivery, but that on it&rsquo;s own lacks a cohesive approach in terms of project planning, and working to an agreed plan.</p>
<p>SDD accelerates the whole project. It does this by shaping requirements and design first. So you have a clear plan that you can act on and your code is generated in line with that.</p>
<p>I also learned to have a backup plan.<br>
Having travelled at the speed of developer light for the first few hours I ran into this issue:</p>
<p><img alt="Kiro says no!" loading="lazy" src="/posts/kiro-and-sdd/Kiro-says-no.png"></p>
<p>There was 10 days before I&rsquo;d get new credits and having generated, but not yet deployed and tested, all of the code&hellip; I couldn&rsquo;t sit around and wait for that.</p>
<p>Most of the heavy lifting was done though - I was at the stage of deploying, reviewing, correcting, until it deployed cleanly and my site looked sensible and stable. But at this juncture I switched to ChatGPT Codex, integrating that with my repos in GitHub to take me down the home straight.</p>
<h2 id="conclusion">Conclusion</h2>
<p>As is hopefully evident, I found Kiro to be incredibly powerful and easy to use. It promoted good practice and made planning quick and easy, but also to the appropriate level, neither under nor over cooked. Although it was rapid - it never felt like I was cutting corners. Quite the opposite in fact, Kiro steers you to think about what you’re doing and helps you to do it quickly.</p>
<p>Working with Kiro is closer to working with people than other code generating tools. I was able to hand off tasks (literally tasks.md) to Kiro while I joined calls or responded to other requests. In that regard, Kiro was like having a team to hand things off to so I could focus on something else, then check in later and course correct.</p>
<p>Kiro&rsquo;s SDD approach promotes a single source of truth that works for an individual or for a project team.
Integrating new project requests would be simplified too - just update the spec and the plan adapts then generates updates for the code automatically.</p>
<p>Teams using Kiro&rsquo;s SDD could be much more competitive in delivering solutions for clients, then can be faster and cheaper.</p>
<p>Business can benefit too. How often is innovation a challenge for organisations because of a lack of available resources? Kiro could allow teams to deliver much more quickly, freeing up time to focus on innovation, or to take on additional projects.</p>
<p><strong>Pricing</strong> will be interesting. At the time I write this, pricing has only been known for a few days and it&rsquo;s higher than expected. Already there are <a href="https://www.theregister.com/2025/08/18/aws_updated_Kiro_pricing/">op-ed articles highlighting how the costs could potentially rise quickly</a>.</p>
<p>I&rsquo;m not massively put off by that yet - I feel like I accidentally stumbled across a viable solution in my own use case: i.e. Use Kiro to go as far as you can, at least completing the project plan, and getting to the generated code ready to test. Then if you&rsquo;re out of credits, switch to another (cheaper) coding assistant that can focus just on generating good quality code.</p>
<p>I would have <em>loved</em> to have completed my blog journey exclusively in Kiro - but I&rsquo;d seen enough in my use case to recognise that there is genuine game changing potential in Kiro&rsquo;s approach.</p>
<p>Right now I&rsquo;m in the early process of learning about two potential projects coming my way. Whether it&rsquo;s one of those or something else, price permitting, I&rsquo;d be very keen to use Kiro to move quickly and accurately.</p>
<h2 id="additional-resources">Additional Resources</h2>
<ul>
<li><a href="https://www.anthropic.com/news/claude-4">Claude-4 from Anthropic</a></li>
<li><a href="https://Kiro.dev/pricing/">Kiro Tiers and Pricing</a></li>
<li><a href="https://gohugo.io/getting-started/">Getting started with Hugo</a></li>
</ul>
<hr>
<p><em>This post is part of my AWS technical blog series. Found this helpful? Connect with me on <a href="https://www.linkedin.com/in/john-mcmillan-aaa4274/">LinkedIn</a> or check back here for more AWS content and discussions.</em></p>
]]></content:encoded><category>AWS</category><category>Kiro</category><category>AI</category><category>AWS</category><category>Kiro</category><category>gen-ai</category><category>Hugo</category><category>AWS Kiro</category><enclosure url="https://aws.mcmillearn.net/images/Kiro-1.png" type="image/png"/></item><item><title>About</title><link>https://aws.mcmillearn.net/about/</link><pubDate>Mon, 15 Jan 2024 10:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/about/</guid><description>About this AWS technical blog and its author</description><content:encoded><![CDATA[<h1 id="about-this-blog">About This Blog</h1>
<p>Welcome to my AWS technical blog! This platform serves as a showcase of AWS expertise and best practices, created as part of my AWS Ambassador application journey.</p>
<h2 id="what-youll-find-here">What You&rsquo;ll Find Here</h2>
<p>This blog focuses on:</p>
<ul>
<li><strong>AWS Best Practices</strong>: Real-world implementations and lessons learned</li>
<li><strong>Architecture Patterns</strong>: Scalable and cost-effective cloud solutions</li>
<li><strong>Serverless Technologies</strong>: Lambda, API Gateway, and event-driven architectures</li>
<li><strong>Infrastructure as Code</strong>: Terraform and CloudFormation examples</li>
<li><strong>Cost Optimization</strong>: Strategies for efficient AWS resource usage</li>
<li><strong>Security</strong>: AWS security best practices and compliance</li>
</ul>
<h2 id="about-the-author">About the Author</h2>
<p>I&rsquo;m a cloud enthusiast passionate about helping others leverage AWS services effectively. Through this blog, I share practical insights gained from hands-on experience with AWS technologies.</p>
<h2 id="technical-details">Technical Details</h2>
<p>This blog itself demonstrates AWS best practices:</p>
<ul>
<li><strong>Static Site Generation</strong>: Built with Hugo for optimal performance</li>
<li><strong>AWS Hosting</strong>: S3 + CloudFront for global content delivery</li>
<li><strong>Infrastructure as Code</strong>: Terraform-managed AWS resources</li>
<li><strong>CI/CD</strong>: GitHub Actions for automated deployment</li>
<li><strong>Cost-Effective</strong>: Designed to operate under $5/month</li>
</ul>
<h2 id="connect-with-me">Connect With Me</h2>
<p>Feel free to reach out through the social links in the footer or engage with the content through comments and discussions.</p>
]]></content:encoded></item><item><title>Archive</title><link>https://aws.mcmillearn.net/archive/</link><pubDate>Mon, 15 Jan 2024 10:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/archive/</guid><description>Browse all blog posts organized chronologically with advanced filtering options</description><content:encoded><![CDATA[<h1 id="blog-archive">Blog Archive</h1>
<p>Browse all posts from our AWS technical blog, organized chronologically with advanced filtering options to help you find exactly what you&rsquo;re looking for.</p>
]]></content:encoded></item><item><title>Search</title><link>https://aws.mcmillearn.net/search/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><author>John McMillan</author><guid>https://aws.mcmillearn.net/search/</guid><description>Search through all AWS tutorials, guides, and best practices</description><content:encoded></content:encoded></item></channel></rss>