<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Purity Chepkemoi</title>
    <description>The latest articles on DEV Community by Purity Chepkemoi (@puritychepkemoi).</description>
    <link>https://dev.to/puritychepkemoi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4022492%2F028657a6-7425-461c-bad0-f5b46918d47e.png</url>
      <title>DEV Community: Purity Chepkemoi</title>
      <link>https://dev.to/puritychepkemoi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/puritychepkemoi"/>
    <language>en</language>
    <item>
      <title>CloudFormation: A Hands-On Journey Through Compute, Nesting, and Custom Resources</title>
      <dc:creator>Purity Chepkemoi</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:20:47 +0000</pubDate>
      <link>https://dev.to/puritychepkemoi/cloudformation-a-hands-on-journey-through-compute-nesting-and-custom-resources-117h</link>
      <guid>https://dev.to/puritychepkemoi/cloudformation-a-hands-on-journey-through-compute-nesting-and-custom-resources-117h</guid>
      <description>&lt;p&gt;As part of my intensive DevSecOps training with ParoCyber, I’ve been deep-diving into AWS CloudFormation.&lt;br&gt;
For these exercises, I am working through the &lt;a href="https://github.com/samuel-nartey/devops-labs/blob/main/aws/lab-03-iam-vpc-ec2-foundations/Cloudformation-beginner-to-intermediate.md" rel="noopener noreferrer"&gt;CloudFormation Mastery Lab Documentation&lt;/a&gt; on GitHub.&lt;/p&gt;

&lt;p&gt;I am writing this article to document my personal walkthrough, observations, and how I tackled the challenges along the way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 3 Levels At A Glance&lt;/strong&gt;&lt;br&gt;
Here is a quick overview of what we are building and what each stage demonstrates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 5&lt;/strong&gt; — Compute &amp;amp; Wiring: Moving beyond static resources to deploy a live, bootstrapped Apache web server on EC2 wrapped inside localized Security Groups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 6&lt;/strong&gt; — Composition (Nested Stacks): Breaking down monolithic templates into a clean, reusable modular architecture managed by a single Parent stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Level 7&lt;/strong&gt; — Dynamic Intelligence (Custom Resources): Extending CloudFormation's native capabilities by using a Python Lambda function to query customized AMI filters dynamically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 5 — Compute &amp;amp; Wiring: EC2, Security Groups &amp;amp; Intrinsic Functions
&lt;/h2&gt;

&lt;p&gt;I saved the code as webserver.yaml and deployed it via the CloudFormation console using t3.micro.&lt;/p&gt;

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

&lt;p&gt;The public IP in the Outputs tab is:&lt;strong&gt;18.234.230.243&lt;/strong&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhaceitm6gq007ua8s24.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhaceitm6gq007ua8s24.png" alt="AWS console" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observation Point&lt;/strong&gt;&lt;br&gt;
Once deployed, I updated the stack to switch the InstanceType from t3.micro to t3.small.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fue5qb0w0qclj965tuada.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fue5qb0w0qclj965tuada.png" alt="AWS Console" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Question: Does the Public IP shown in the Outputs tab change after this update?&lt;/strong&gt;&lt;em&gt;Yes, it changes, InstanceType can be updated in place for many instance families, but the public IP is not guaranteed to persist across a stop-and-start cycle unless you're using an Elastic IP. CloudFormation performs this as a "stop, resize, start" operation under the hood for compatible type changes, and AWS reassigns a new public IPv4 address on start unless one was reserved. Refresh the Outputs tab after the update completes and compare the new PublicIp value to the one you noted before. This is why production architectures almost never rely on raw instance public IPs, they sit behind an Elastic Load Balancer (which you'll build in Level 9) or use an Elastic IP resource explicitly.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;Level 5 Challenge: Stabilizing the IP&lt;/strong&gt;&lt;br&gt;
To make the public IP persist across instance replacement , I modified the template to introduce an AWS::EC2::EIP resource and an AWS::EC2::EIPAssociation.In the Outputs tab that the public IP now stays the same&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2fydsdm3xj6b74d1pez.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2fydsdm3xj6b74d1pez.png" alt="AWS Console" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 6 — Composition: Nested Stacks
&lt;/h2&gt;

&lt;p&gt;Because nested templates must be fetched programmatically by the CloudFormation engine, I first had to spin up an S3 bucket and upload the child templates.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71w24sbtrxt0ohy5aiz5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F71w24sbtrxt0ohy5aiz5.png" alt="AWS Console" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Deploy the root stack&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qylyyhskiluofez824a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2qylyyhskiluofez824a.png" alt="AWS Console" width="800" height="414"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgyl69j65nmp6vlks02t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxgyl69j65nmp6vlks02t.png" alt="AWS Console" width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Add a third nested stack, a security.yaml template that creates a standalone AWS::EC2::SecurityGroup, upload it to the same S3 bucket, then modify compute.yaml to accept that Security Group ID as a Parameter instead of creating its own. This mirrors how real platform teams separate "network," "security," and "compute" ownership across different template authors.&lt;/em&gt;&lt;br&gt;
I added a third nested stack, &lt;strong&gt;SecurityStack&lt;/strong&gt;, using a new &lt;strong&gt;security.yaml&lt;/strong&gt; template. This template creates a standalone &lt;code&gt;AWS::EC2::SecurityGroup&lt;/code&gt; and accepts the VPC ID from the parent stack.&lt;/p&gt;

&lt;p&gt;The architecture is now split into three reusable components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;network.yaml&lt;/strong&gt;: Creates the VPC and subnet; outputs &lt;code&gt;VpcId&lt;/code&gt; and &lt;code&gt;SubnetId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;security.yaml&lt;/strong&gt;: Accepts &lt;code&gt;VpcId&lt;/code&gt;, creates the web server security group allowing HTTP (port 80), and outputs &lt;code&gt;SecurityGroupId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compute.yaml&lt;/strong&gt;: No longer creates a security group; accepts &lt;code&gt;SecurityGroupId&lt;/code&gt; and uses it in the EC2 instance &lt;code&gt;SecurityGroupIds&lt;/code&gt; property.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I uploaded &lt;strong&gt;security.yaml&lt;/strong&gt; to the same S3 bucket as the other nested templates and updated &lt;strong&gt;root.yaml&lt;/strong&gt; to add the new nested stack.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  Level 7 — Dynamic Intelligence: Custom Resources
&lt;/h2&gt;

&lt;p&gt;I uploaded &lt;code&gt;custom-resource.yaml&lt;/code&gt;, named the stack &lt;strong&gt;level7-custom-resource&lt;/strong&gt;, and proceeded through the wizard. On the &lt;strong&gt;Review&lt;/strong&gt; screen, I saw a new section near the bottom labeled &lt;strong&gt;"I acknowledge that AWS CloudFormation might create IAM resources."&lt;/strong&gt; I checked this box, which serves as the console's version of the &lt;code&gt;CAPABILITY_IAM&lt;/code&gt; flag, before submitting the stack. After the stack reached &lt;strong&gt;CREATE_COMPLETE&lt;/strong&gt;, I opened the &lt;strong&gt;Outputs&lt;/strong&gt; tab and viewed the &lt;strong&gt;ResolvedAmiId&lt;/strong&gt; value that the Lambda function had automatically discovered.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjk5abfdknej5ozxfp31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpjk5abfdknej5ozxfp31.png" alt="AWS Console" width="800" height="416"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feydexta9gbdpvq126ebv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feydexta9gbdpvq126ebv.png" alt="AWS Console" width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;&lt;br&gt;
Modify the Lambda to also handle the Delete RequestType meaningfully, have it log the AMI ID it's "cleaning up" (even though there's nothing to actually delete in this case) to prove you understand that every Custom Resource lifecycle event needs explicit handling. Check CloudWatch Logs (search "CloudWatch" in the console, then Log groups) to confirm your log line appears after you delete the stack.&lt;br&gt;
I modified the Lambda function to handle the &lt;strong&gt;Delete&lt;/strong&gt; &lt;code&gt;RequestType&lt;/code&gt; by logging the AMI ID it was "cleaning up," even though there was no actual resource to delete. This demonstrated my understanding that every Custom Resource lifecycle event requires explicit handling. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjahu2zghoh0nxfzxvxx3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjahu2zghoh0nxfzxvxx3.png" alt="AWS Console" width="800" height="431"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foney9ruccpkc8hb1nqjk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foney9ruccpkc8hb1nqjk.png" alt="AWS Console" width="800" height="409"&gt;&lt;/a&gt;&lt;br&gt;
After deleting the stack, I checked the &lt;strong&gt;CloudWatch Logs&lt;/strong&gt; by navigating to &lt;strong&gt;CloudWatch&lt;/strong&gt; and opening the &lt;strong&gt;Log groups&lt;/strong&gt; section, where I confirmed that the expected log entry had been recorded.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faodfkw41n3jroqj90rnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faodfkw41n3jroqj90rnm.png" alt="AWS Console" width="799" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloudformation</category>
      <category>infrastructureascode</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
