DEV Community

Purity Chepkemoi
Purity Chepkemoi

Posted on

CloudFormation: A Hands-On Journey Through Compute, Nesting, and Custom Resources

As part of my intensive DevSecOps training with ParoCyber, I’ve been deep-diving into AWS CloudFormation.
For these exercises, I am working through the CloudFormation Mastery Lab Documentation on GitHub.

I am writing this article to document my personal walkthrough, observations, and how I tackled the challenges along the way.

The 3 Levels At A Glance
Here is a quick overview of what we are building and what each stage demonstrates:

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

Level 6 — Composition (Nested Stacks): Breaking down monolithic templates into a clean, reusable modular architecture managed by a single Parent stack.

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

Level 5 — Compute & Wiring: EC2, Security Groups & Intrinsic Functions

I saved the code as webserver.yaml and deployed it via the CloudFormation console using t3.micro.

AWS Console

The public IP in the Outputs tab is:18.234.230.243AWS console

Observation Point
Once deployed, I updated the stack to switch the InstanceType from t3.micro to t3.small.
AWS Console

AWS Console

Question: Does the Public IP shown in the Outputs tab change after this update?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.
Level 5 Challenge: Stabilizing the IP
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
AWS Console

Level 6 — Composition: Nested Stacks

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.
AWS Console

AWS Console

Deploy the root stack
AWS Console
AWS Console

Challenge
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.
I added a third nested stack, SecurityStack, using a new security.yaml template. This template creates a standalone AWS::EC2::SecurityGroup and accepts the VPC ID from the parent stack.

The architecture is now split into three reusable components:

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

I uploaded security.yaml to the same S3 bucket as the other nested templates and updated root.yaml to add the new nested stack.

AWS Console

AWS Console

Level 7 — Dynamic Intelligence: Custom Resources

I uploaded custom-resource.yaml, named the stack level7-custom-resource, and proceeded through the wizard. On the Review screen, I saw a new section near the bottom labeled "I acknowledge that AWS CloudFormation might create IAM resources." I checked this box, which serves as the console's version of the CAPABILITY_IAM flag, before submitting the stack. After the stack reached CREATE_COMPLETE, I opened the Outputs tab and viewed the ResolvedAmiId value that the Lambda function had automatically discovered.
AWS Console
AWS Console

Challenge
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.
I modified the Lambda function to handle the Delete RequestType 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.

AWS Console
AWS Console
After deleting the stack, I checked the CloudWatch Logs by navigating to CloudWatch and opening the Log groups section, where I confirmed that the expected log entry had been recorded.
AWS Console

Top comments (0)