Final Project: Build It, Break It, Fix It

This AWS final project pulls the whole course together: you build a small company’s web stack from one CloudFormation template, then a script breaks it in 10 different ways, one at a time, the way real production breaks. Your job is to find the cause with the tools you’ve built and fix it properly, as you would on call.

Two scenarios are worked through step by step in this lesson. The other eight are yours to solve, with the answer key at the end of the “Break it” section. Try each one for 20 minutes before you look.

TimeYou needBy the end you can
3 to 4 hours (do it over a couple of sittings if you like; stop the clock between scenarios)Phases 1 to 4 done, with Get-RecentChanges.ps1, Test-Ec2Access.ps1, Find-Spenders.ps1 and Find-Waste.ps1 in C:\aws-lab, SNS topic saat-alerts, saat-vpc, signed in as saat-admin. Cost: about USD 0.10 an hour while the stack runs (an ALB and two t3.micro instances); delete it at the end of each sitting if you take a break.Deploy a multi-tier stack from a template, check its health quickly, and diagnose and fix load balancer, Auto Scaling, network, IAM-adjacent, cost, alarm, drift and launch template failures using everything from the course.

What it is, in one line

The AWS final project is a hands-on test: build SaatShop online with CloudFormation, then diagnose and fix 10 deliberately injected failures using the course’s troubleshooting methods.

In Simple Words

The driving test

Learning to drive happens one skill at a time: steering, braking, mirrors, parking. The driving test puts them together on a real road, where you don’t know what comes next.

This project is your driving test. Each scenario is a surprise on the road: you notice the symptom, decide what to check, and act calmly, in the right order.

The examiner doesn’t care how fast you guess. They care that you checked your mirrors first.

Engineer's View

What you’re building

One template creates SaatShop online in saat-vpc: an Application Load Balancer in the public subnets (WebSg), a target group with a /health check on port 8080, an Auto Scaling group of two t3.micro instances in the private subnets (AppSg plus a stack-managed admin security group), a launch template with the Python app from Lesson 66, an S3 bucket for assets, and a CloudWatch alarm on UnHealthyHostCount that notifies saat-alerts.

The Auto Scaling group uses ELB health checks, so it replaces instances the load balancer marks unhealthy. That’s realistic, and it also means some failures look like instances “disappearing” or cycling, a pattern you’ll recognise afterwards.

Break-SaatShop.ps1 injects one scenario at a time. Get-ShopHealth gives you a one-screen view: site response, alarm state, Auto Scaling counts and target health with reasons. Start every scenario there, then apply the four questions from Lesson 56.

How it works, step by step

Here is the routine for every scenario.

1Look before you touch

Run Get-ShopHealth and write down the symptom: code, timing, which targets, alarm state.

SaatShop online architecture: ALB, Auto Scaling group, alarm and S3 in saat-vpc

2Ask what changed

Get-RecentChanges.ps1 for the last 15 minutes (remember the delay), in the right Region. Then pick the matching lesson’s tool.

3Fix narrowly, then confirm

Undo only the faulty change, run Get-ShopHealth again, and note what guardrail would have prevented it.

4Use the toolkit

Every scenario can be solved with tools from earlier lessons:

Troubleshooting toolkit from the AWS Cloud Administration course

Keep notes as you go: symptom, first check, cause, fix, guardrail. Those notes are exactly what interviewers ask about (Lesson 70).

✅ Checkpoint: You can describe SaatShop online’s parts, why ELB health checks make the ASG replace instances, and your routine for each scenario.

Hands-on lab: build SaatShop online and solve 10 scenarios

Sign in and save the template:

cd C:\aws-lab
aws login --profile saat-admin
$env:AWS_PROFILE = 'saat-admin'
$v = Get-Content C:\aws-lab\saat-vpc.json | ConvertFrom-Json
$tpl = @'
AWSTemplateFormatVersion: '2010-09-09'
Description: SaatVedha final project - SaatShop online
Parameters:
  VpcId:
    Type: AWS::EC2::VPC::Id
  PublicSubnets:
    Type: List<AWS::EC2::Subnet::Id>
  PrivateSubnets:
    Type: List<AWS::EC2::Subnet::Id>
  AlbSg:
    Type: AWS::EC2::SecurityGroup::Id
  AppSg:
    Type: AWS::EC2::SecurityGroup::Id
  AlertTopic:
    Type: String
Resources:
  AppRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
  AppProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref AppRole
  AdminSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SaatShop admin access (no rules on purpose)
      VpcId: !Ref VpcId
  AppLT:
    Type: AWS::EC2::LaunchTemplate
    Properties:
      LaunchTemplateData:
        ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64}}'
        InstanceType: t3.micro
        IamInstanceProfile:
          Arn: !GetAtt AppProfile.Arn
        SecurityGroupIds:
          - !Ref AppSg
          - !Ref AdminSg
        MetadataOptions:
          HttpTokens: required
        TagSpecifications:
          - ResourceType: instance
            Tags:
              - Key: Name
                Value: saat-final-app
              - Key: course
                Value: saatvedha-aws
        UserData:
          Fn::Base64: |
            #!/bin/bash
            cat > /opt/app.py <<'EOF2'
            import http.server, socket
            class H(http.server.BaseHTTPRequestHandler):
                def do_GET(self):
                    if self.path not in ('/', '/health'):
                        self.send_error(404)
                        return
                    body = ('SaatShop online from ' + socket.gethostname() + '\n').encode()
                    self.send_response(200)
                    self.send_header('Content-Length', str(len(body)))
                    self.end_headers()
                    self.wfile.write(body)
                def log_message(self, *args):
                    pass
            http.server.ThreadingHTTPServer(('', 8080), H).serve_forever()
            EOF2
            systemd-run --unit=app8080 python3 /opt/app.py
  AppTG:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      VpcId: !Ref VpcId
      Protocol: HTTP
      Port: 8080
      TargetType: instance
      HealthCheckPath: /health
      HealthCheckIntervalSeconds: 10
      HealthyThresholdCount: 2
      UnhealthyThresholdCount: 2
  Alb:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Type: application
      Subnets: !Ref PublicSubnets
      SecurityGroups:
        - !Ref AlbSg
  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref Alb
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref AppTG
  AppAsg:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      MinSize: '2'
      MaxSize: '4'
      DesiredCapacity: '2'
      VPCZoneIdentifier: !Ref PrivateSubnets
      LaunchTemplate:
        LaunchTemplateId: !Ref AppLT
        Version: !GetAtt AppLT.LatestVersionNumber
      TargetGroupARNs:
        - !Ref AppTG
      HealthCheckType: ELB
      HealthCheckGracePeriod: 120
  AssetBucket:
    Type: AWS::S3::Bucket
  UnhealthyAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: SaatShop has unhealthy app servers
      Namespace: AWS/ApplicationELB
      MetricName: UnHealthyHostCount
      Dimensions:
        - Name: TargetGroup
          Value: !GetAtt AppTG.TargetGroupFullName
        - Name: LoadBalancer
          Value: !GetAtt Alb.LoadBalancerFullName
      Statistic: Maximum
      Period: 60
      EvaluationPeriods: 2
      Threshold: 0
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching
      AlarmActions:
        - !Ref AlertTopic
Outputs:
  AlbDns:
    Value: !GetAtt Alb.DNSName
  AlbFullName:
    Value: !GetAtt Alb.LoadBalancerFullName
  AsgName:
    Value: !Ref AppAsg
  TgArn:
    Value: !Ref AppTG
  LtId:
    Value: !Ref AppLT
  AlarmName:
    Value: !Ref UnhealthyAlarm
  Bucket:
    Value: !Ref AssetBucket
  AdminSgId:
    Value: !Ref AdminSg
'@
$tpl | Set-Content final.yaml
aws cloudformation validate-template --template-body file://final.yaml --query "Capabilities" --output text

You should see this:

CAPABILITY_IAM

1Deploy SaatShop online

Paste this whole block (about 6 minutes):

$topic = aws sns list-topics --query "Topics[?ends_with(TopicArn, ':saat-alerts')].TopicArn | [0]" --output text
@(
  @{ ParameterKey = 'VpcId'; ParameterValue = $v.VpcId },
  @{ ParameterKey = 'PublicSubnets'; ParameterValue = "$($v.PublicA),$($v.PublicB)" },
  @{ ParameterKey = 'PrivateSubnets'; ParameterValue = "$($v.PrivateA),$($v.PrivateB)" },
  @{ ParameterKey = 'AlbSg'; ParameterValue = $v.WebSg },
  @{ ParameterKey = 'AppSg'; ParameterValue = $v.AppSg },
  @{ ParameterKey = 'AlertTopic'; ParameterValue = $topic }) | ConvertTo-Json | Set-Content final-params.json
aws cloudformation create-stack --stack-name saat-final --template-body file://final.yaml --parameters file://final-params.json --capabilities CAPABILITY_IAM --tags Key=course,Value=saatvedha-aws Key=lesson,Value=69 | Out-Null
aws cloudformation wait stack-create-complete --stack-name saat-final
aws cloudformation describe-stacks --stack-name saat-final --query "Stacks[0].StackStatus" --output text

You should see this:

CREATE_COMPLETE

2Check its health

Paste this whole block. It loads the stack outputs and two helpers, then waits until both servers are healthy:

$o = @{}
aws cloudformation describe-stacks --stack-name saat-final --query "Stacks[0].Outputs" --output json | ConvertFrom-Json | ForEach-Object { $o[$_.OutputKey] = $_.OutputValue }
function Get-Code([string]$Path) {
  $sw = [Diagnostics.Stopwatch]::StartNew()
  try { $r = Invoke-WebRequest "http://$($o.AlbDns)$Path" -UseBasicParsing -TimeoutSec 30; $code = $r.StatusCode; $body = $r.Content.Trim() } catch { $code = [int]$_.Exception.Response.StatusCode; $body = '' }
  '{0}  {1}  {2:N1} s  {3}' -f $Path, $code, $sw.Elapsed.TotalSeconds, $body
}
function Get-ShopHealth {
  "Site:   $(Get-Code /)"
  "Alarm:  $(aws cloudwatch describe-alarms --alarm-names $o.AlarmName --query 'MetricAlarms[0].StateValue' --output text)"
  "ASG:    $(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $o.AsgName --query 'AutoScalingGroups[0].[MinSize,DesiredCapacity,length(Instances)]' --output text)"
  aws elbv2 describe-target-health --target-group-arn $o.TgArn --query "TargetHealthDescriptions[].[Target.Id,TargetHealth.State,TargetHealth.Reason]" --output text
}
do { Start-Sleep 20; $h = aws elbv2 describe-target-health --target-group-arn $o.TgArn --query "length(TargetHealthDescriptions[?TargetHealth.State=='healthy'])" --output text } until ($h -eq '2')
Get-ShopHealth

You should see something like this:

Site:   /  200  0.1 s  SaatShop online from ip-10-20-11-201.ap-south-1.compute.internal
Alarm:  OK
ASG:    2       2       2
i-0a1b2c3d4e5f60718     healthy None
i-0b2c3d4e5f6a70819     healthy None

This is “normal”. Take a screenshot; after every fix, Get-ShopHealth should look like this again.

EC2 Auto Scaling group of the saat-final stack with 2 of 2 instances in service and healthy
The “normal” picture in the console: two healthy instances in service. After every fix, the shop should look like this again.
AWS final project lab output: healthy SaatShop online
SaatShop online, healthy. Something like this.

3Save the scenario script

Paste this whole block. Break-SaatShop.ps1 injects one failure each time you run it:

$b = @'
param([Parameter(Mandatory)][ValidateRange(1, 10)][int]$Scenario)
$o = @{}
aws cloudformation describe-stacks --stack-name saat-final --query "Stacks[0].Outputs" --output json | ConvertFrom-Json | ForEach-Object { $o[$_.OutputKey] = $_.OutputValue }
$v = Get-Content C:\aws-lab\saat-vpc.json | ConvertFrom-Json
$ids = @((aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $o.AsgName --query "AutoScalingGroups[0].Instances[].InstanceId" --output text) -split '\s+' | Where-Object { $_ })
switch ($Scenario) {
  1 { aws autoscaling update-auto-scaling-group --auto-scaling-group-name $o.AsgName --min-size 0 --desired-capacity 0 }
  2 { aws ec2 revoke-security-group-ingress --group-id $v.AppSg --ip-permissions "IpProtocol=tcp,FromPort=8080,ToPort=8080,UserIdGroupPairs=[{GroupId=$($v.WebSg)}]" | Out-Null }
  3 { aws elbv2 modify-target-group --target-group-arn $o.TgArn --health-check-path /status | Out-Null }
  4 {
    $assoc = aws ec2 describe-network-acls --filters "Name=association.subnet-id,Values=$($v.PrivateA)" --query "NetworkAcls[0].Associations[?SubnetId=='$($v.PrivateA)'].NetworkAclAssociationId | [0]" --output text
    $orig = aws ec2 describe-network-acls --filters "Name=association.subnet-id,Values=$($v.PrivateA)" --query "NetworkAcls[0].NetworkAclId" --output text
    $nacl = aws ec2 create-network-acl --vpc-id $v.VpcId --tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=saat-final-nacl},{Key=course,Value=saatvedha-aws}]' --query NetworkAcl.NetworkAclId --output text
    aws ec2 create-network-acl-entry --network-acl-id $nacl --ingress --rule-number 100 --protocol tcp --port-range From=8080,To=8080 --cidr-block 10.20.0.0/16 --rule-action allow
    $new = aws ec2 replace-network-acl-association --association-id $assoc --network-acl-id $nacl --query NewAssociationId --output text
    @{ OriginalAcl = $orig; NewAssociation = $new; BadAcl = $nacl } | ConvertTo-Json | Set-Content C:\aws-lab\final-nacl.json
  }
  5 { aws ec2 stop-instances --instance-ids $ids[0] | Out-Null }
  6 {
    'Diwali sale: 20% off' | Set-Content promo.txt
    aws s3 cp promo.txt "s3://$($o.Bucket)/promo.txt" --only-show-errors
    aws s3 presign "s3://$($o.Bucket)/promo.txt" --expires-in 5 | Set-Content C:\aws-lab\final-link.txt
    Remove-Item promo.txt
  }
  7 {
    $topic = aws cloudwatch describe-alarms --alarm-names $o.AlarmName --query "MetricAlarms[0].AlarmActions[0]" --output text
    aws cloudwatch put-metric-alarm --alarm-name $o.AlarmName --namespace AWS/ApplicationELB --metric-name UnHealthyHostCount --dimensions "Name=LoadBalancer,Value=$($o.AlbFullName)" --statistic Maximum --period 60 --evaluation-periods 2 --threshold 0 --comparison-operator GreaterThanThreshold --alarm-actions $topic
  }
  8 { aws ec2 authorize-security-group-ingress --group-id $o.AdminSgId --protocol tcp --port 22 --cidr 0.0.0.0/0 | Out-Null }
  9 { aws ec2 allocate-address --region ap-southeast-1 --domain vpc --query PublicIp --output text | Out-Null }
  10 {
    $ud = aws ec2 describe-launch-template-versions --launch-template-id $o.LtId --versions 1 --query "LaunchTemplateVersions[0].LaunchTemplateData.UserData" --output text
    $text = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($ud)).Replace([string][char]10, [string][char]13 + [string][char]10)
    '{ "UserData": "B64" }'.Replace('B64', [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($text))) | Set-Content lt-data.json
    $ver = aws ec2 create-launch-template-version --launch-template-id $o.LtId --source-version 1 --launch-template-data file://lt-data.json --query LaunchTemplateVersion.VersionNumber --output text
    aws autoscaling update-auto-scaling-group --auto-scaling-group-name $o.AsgName --launch-template "LaunchTemplateId=$($o.LtId),Version=$ver"
    aws autoscaling terminate-instance-in-auto-scaling-group --instance-id $ids[0] --no-should-decrement-desired-capacity | Out-Null
    Remove-Item lt-data.json
  }
}
"Scenario $Scenario is live. Find out what broke, then fix it."
'@
$b | Set-Content C:\aws-lab\Break-SaatShop.ps1
Get-ChildItem C:\aws-lab\*.ps1 | Select-Object -ExpandProperty Name

You should see something like this:

Break-SaatShop.ps1
Find-Spenders.ps1
Find-Waste.ps1
Get-RecentChanges.ps1
Remove-LabBucket.ps1
Start-SqlLab.ps1
Start-SsmEndpoints.ps1
Stop-SqlLab.ps1
Stop-SsmEndpoints.ps1
Test-Ec2Access.ps1
Test-NativeRestore.ps1
Test-RdsAccess.ps1

Don’t read the script yet if you want a fair test. Its switch block is the list of answers.

4Work through the scenarios

Run one scenario at a time with .\Break-SaatShop.ps1 -Scenario N, investigate, fix it, and confirm with Get-ShopHealth before the next. Here is what the “users” report for each one, and a hint about where to start:

#What users reportStart with
1“The website shows Service Unavailable.”Get-ShopHealth, Auto Scaling counts
2“Pages hang for 10 seconds, then fail.”Target health reasons, Lesson 66
3“Site works, but the unhealthy alarm went off.”Target health reasons, alarm
4“Half our capacity is gone, one server keeps failing.”Target health, subnet, NACL (worked below)
5“A server vanished and a new one appeared.”Get-RecentChanges, Auto Scaling activities
6“Marketing’s promo link gives an error.”The link in C:\aws-lab\final-link.txt, Lesson 60
7“The unhealthy alarm never fires any more.”Alarm state and its dimensions, Lesson 23
8“Security says something changed on our servers.”Drift detection, Lesson 67
9“Finance sees a new small charge every day.”Find-Spenders, Lesson 68
10“After a config change, new servers never come up.”Scaling activities, console output (worked below)

✅ Checkpoint: saat-final is CREATE_COMPLETE, Get-ShopHealth shows 200, alarm OK and two healthy targets, and Break-SaatShop.ps1 is saved.

Break It, Then Fix It

Here are scenarios 4 and 10, worked from symptom to fix. Solve the other eight yourself, then compare with the answer key below.

1Worked scenario 4: half the capacity is gone

Inject it, wait for health checks, and look:

.\Break-SaatShop.ps1 -Scenario 4
Start-Sleep 60
Get-ShopHealth
$bad = aws elbv2 describe-target-health --target-group-arn $o.TgArn --query "TargetHealthDescriptions[?TargetHealth.State=='unhealthy'].Target.Id | [0]" --output text
$sub = aws ec2 describe-instances --instance-ids $bad --query "Reservations[0].Instances[0].SubnetId" --output text
aws ec2 describe-network-acls --filters "Name=association.subnet-id,Values=$sub" --query "NetworkAcls[0].[NetworkAclId,Tags[?Key=='Name'].Value | [0],length(Entries[?Egress])]" --output text

You’ll get something like this:

Site:   /  200  0.1 s  SaatShop online from ip-10-20-12-77.ap-south-1.compute.internal
Alarm:  ALARM
ASG:    2       2       2
i-0a1b2c3d4e5f60718     unhealthy       Target.Timeout
i-0b2c3d4e5f6a70819     healthy None
acl-0f1e2d3c4b5a69788   saat-final-nacl 1

The site still answers (from the healthy server), the alarm fired, and the failing server’s subnet now has a custom NACL whose only outbound entry is the default deny. Health check requests get in on 8080 but the replies can’t leave: Target.Timeout. Because the ASG uses ELB health checks, it will keep replacing that server, and the replacement fails the same way.

Fix: put the subnet back on its original NACL and remove the bad one:

$n = Get-Content C:\aws-lab\final-nacl.json | ConvertFrom-Json
aws ec2 replace-network-acl-association --association-id $n.NewAssociation --network-acl-id $n.OriginalAcl | Out-Null
aws ec2 delete-network-acl --network-acl-id $n.BadAcl
Remove-Item C:\aws-lab\final-nacl.json
Start-Sleep 90
Get-ShopHealth

You should see two healthy targets again, and the alarm back to OK within a few minutes.

2Worked scenario 10: new servers never come up

Inject it, give the replacement a few minutes, then follow the evidence:

.\Break-SaatShop.ps1 -Scenario 10
Start-Sleep 300
Get-ShopHealth
aws autoscaling describe-scaling-activities --auto-scaling-group-name $o.AsgName --max-items 3 --query "Activities[].[StatusCode,Description]" --output text
$new = aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $o.AsgName --query "AutoScalingGroups[0].Instances[?LaunchTemplate.Version!='1'].InstanceId | [0]" --output text
aws ec2 get-console-output --instance-id $new --latest --query Output --output text | Select-String 'bad interpreter' | Select-Object -First 1

You’ll get something like this:

Site:   /  200  0.1 s  SaatShop online from ip-10-20-12-77.ap-south-1.compute.internal
Alarm:  ALARM
ASG:    2       2       2
i-0c3d4e5f6a7b80912     unhealthy       Target.FailedHealthChecks
i-0b2c3d4e5f6a70819     healthy None
InProgress      Launching a new EC2 instance: i-0d4e5f6a7b8c90123
Successful      Terminating EC2 instance: i-0c2b3a4d5e6f70811
Successful      Launching a new EC2 instance: i-0c2b3a4d5e6f70811
[   48.120000] cloud-init[1203]: /var/lib/cloud/instance/scripts/part-001: /bin/bash^M: bad interpreter: No such file or directory
Final project worked scenarios: NACL blocking one subnet and CRLF launch template
Both worked scenarios and their causes. Something like this.

The group launches a server, ELB marks it unhealthy because the app never started, the group terminates it and tries again: a replacement loop. The console output names the cause from Lesson 11: the new launch template version’s user data has Windows line endings, so the script never runs.

Fix: point the group back at the good version 1 and replace the broken server (in real life you’d also delete the bad version and fix the pipeline that made it):

aws autoscaling update-auto-scaling-group --auto-scaling-group-name $o.AsgName --launch-template "LaunchTemplateId=$($o.LtId),Version=1"
$broken = aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names $o.AsgName --query "AutoScalingGroups[0].Instances[?LaunchTemplate.Version!='1'].InstanceId" --output text
foreach ($x in ($broken -split '\s+')) { if ($x) { aws autoscaling terminate-instance-in-auto-scaling-group --instance-id $x --no-should-decrement-desired-capacity | Out-Null } }
Start-Sleep 240
Get-ShopHealth

You should see two healthy targets again.

3Answer key for the other scenarios

Only read a row after you’ve tried that scenario yourself:

#CauseFix
1Auto Scaling min and desired set to 0: no targets, so 503update-auto-scaling-group --min-size 2 --desired-capacity 2
2AppSg lost its 8080 rule from WebSg: Target.Timeout and 504Re-authorize 8080 from WebSg on AppSg
3Health check path changed to /status: ResponseCodeMismatch, fail-open keeps the site upmodify-target-group --health-check-path /health
5Someone stopped a server; ELB health checks made the ASG replace itGet-RecentChanges shows StopInstances and who; talk to them; nothing to restore
6The presigned URL expired after 5 seconds: 403 Request has expiredGenerate a new link with a sensible --expires-in
7The alarm was edited to use only the LoadBalancer dimension: no dataPut both TargetGroup and LoadBalancer dimensions back (or update the stack)
8Port 22 from 0.0.0.0/0 added to the stack’s admin security group: driftRevoke the rule; detect-stack-drift shows IN_SYNC
9An unused Elastic IP in ap-southeast-1: idle IPv4 chargeFind-Spenders finds it; release-address in that Region

Now clean up. The first lines undo any scenario you didn’t finish:

aws ec2 authorize-security-group-ingress --group-id $v.AppSg --ip-permissions "IpProtocol=tcp,FromPort=8080,ToPort=8080,UserIdGroupPairs=[{GroupId=$($v.WebSg),Description=8080-from-web-sg}]" 2>$null | Out-Null
if (Test-Path C:\aws-lab\final-nacl.json) { $n = Get-Content C:\aws-lab\final-nacl.json | ConvertFrom-Json; aws ec2 replace-network-acl-association --association-id $n.NewAssociation --network-acl-id $n.OriginalAcl | Out-Null; aws ec2 delete-network-acl --network-acl-id $n.BadAcl; Remove-Item C:\aws-lab\final-nacl.json }
$eips = aws ec2 describe-addresses --region ap-southeast-1 --query "Addresses[?AssociationId==null].AllocationId" --output text
foreach ($x in ($eips -split '\s+')) { if ($x) { aws ec2 release-address --region ap-southeast-1 --allocation-id $x } }
aws s3 rm "s3://$($o.Bucket)" --recursive --only-show-errors
aws cloudformation delete-stack --stack-name saat-final
aws cloudformation wait stack-delete-complete --stack-name saat-final
Remove-Item final.yaml, final-params.json, C:\aws-lab\final-link.txt -ErrorAction SilentlyContinue
.\Find-Spenders.ps1

The last command should report nothing running or left behind.

✅ Checkpoint: You solved scenarios 4 and 10 from evidence, worked through the other eight with the answer key, and deleted saat-final. Find-Spenders.ps1 finds nothing left behind.

This happened at work

What on-call really feels like

A new cloud engineer at a fintech joined the on-call rotation after three weeks. Her first page at 1 a.m. said “checkout errors rising”. She didn’t know the system well, so she followed the routine she’d practised: look, ask what changed, then use the tool for that layer.

Target health showed one Availability Zone’s servers timing out. CloudTrail showed a network ACL change an hour earlier, made by an automation role during a “hardening” run. She reverted the association, confirmed recovery, and wrote down the guardrail: run NACL changes through review, with a canary subnet first.

Her senior colleague, woken by the same page, arrived to find it fixed. What impressed the team wasn’t speed, but her notes: symptom, evidence, cause, fix and prevention, in that order.

Common mistakes and troubleshooting

MistakeWhat happensWhat to do
Changing things before lookingYou add a second problem to the firstGet-ShopHealth and notes first
Forgetting ELB health checks replace instancesEvidence disappears with the old serverRead scaling activities; act on the cause
Checking CloudTrail too soon“Nothing changed”Wait a few minutes, widen the window
Only looking in one RegionMissing a leak in anotherFind-Spenders across all Regions
Fixing out of band without telling CloudFormationDrift and future rollback failuresFix in the template, or revert to it
No notesSame incident, same confusion next timeSymptom, cause, fix, guardrail

Interview questions

Click a question to open it. Each one is written the way an interviewer would ask it. Under it you will see why they ask it and one way you could answer, in your own words.

Fresher level

1. Your company website on AWS is down. Walk me through how you’d troubleshoot it.

Why they ask this: They want to see that you follow a clear method under pressure, and that you finish by preventing the same outage.

How you might answer: First I look at the symptom: the error codes, when it started, and the target health. Then I ask what changed, using CloudTrail, and check each layer in turn: the ALB, the Auto Scaling group, the network, then the app.

I make the smallest fix that works and confirm the site is back. Afterwards I add a guardrail so it doesn’t happen again.

2. Can you explain what setting the health check type to ELB does in an Auto Scaling group?

Why they ask this: They want to check that you understand how the load balancer and Auto Scaling work together to replace bad instances.

How you might answer: With ELB health checks on, the Auto Scaling group trusts the load balancer. If the load balancer marks an instance unhealthy, the group treats it as unhealthy too and replaces it.

Without it, the group only replaces instances that fail the EC2 status checks.

3. What is a launch template version, and why does it matter which one your Auto Scaling group uses?

Why they ask this: They want to see that you know how instance settings are versioned, because the wrong version can roll out a bad change.

How you might answer: A launch template version is a copy of the launch settings that can’t be changed once it’s saved. The Auto Scaling group uses a specific version number, or $Default, or $Latest.

If it points to $Latest, any new version goes live on the next launch, so I’m careful with that.

2–5 years experience

4. New instances in an Auto Scaling group keep getting launched and replaced. How would you investigate?

Why they ask this: They want to see that you can break a replacement loop by checking the right places in a sensible order.

How you might answer: I start with the scaling activities, which give the reason for each replacement, and the target health reasons. Then I read the console output for boot or user-data errors.

I also check for recent launch template or AMI changes, and the security groups and NACLs for the new subnet.

5. The site is working fine, but a health alarm keeps firing. What would you check?

Why they ask this: They want to see if you know the site can work while every target is unhealthy, and that you don’t ignore that alarm.

How you might answer: I’d check the target health reasons. Often the health check path or port is wrong, so the targets look unhealthy even though the app is fine.

If all targets are unhealthy, the ALB fails open and sends traffic to them anyway, which is why the site still works. I’d fix the health check before a real failure hides behind it.

6. How would you help your team get better at handling incidents before a real one happens?

Why they ask this: They want to see that you value practice, and know how to run it safely and without blame.

How you might answer: I’d run game days in a test environment, breaking things on purpose with scripts or AWS Fault Injection Service. The team would follow the runbooks, and we’d time each exercise.

Afterwards we’d hold a blameless review and update the runbooks with what we learned.

Senior level

7. How would you turn the SaatShop online lab into a resilient production setup?

Why they ask this: They want to see that you can design a complete production system, covering availability, security, operations and cost.

How you might answer: I’d run a Multi-AZ ALB and Auto Scaling group with health checks and warm-up, HTTPS with ACM, WAF in front, and RDS Multi-AZ for the database. All changes would go through IaC pipelines with change sets and drift detection.

I’d centralise logs and metrics, set alarms that link to runbooks, and add cost budgets, anomaly detection and SCP guardrails.

8. Your team takes too long to recover from incidents. What would you do to bring that time down?

Why they ask this: They want to see that you know recovery speed comes from preparation, tooling and practice, not from heroes.

How you might answer: I’d build a clear dashboard that shows health on one screen, and tune the alarms so they’re useful. I’d make recent changes easy to see, and write runbooks with the actual commands.

Then we’d practise with game days, and make sure every release has a fast, safe way to roll back.

Quick quiz

Q1. What is the first thing to do in every scenario?

  1. Restart everything
  2. Look: Get-ShopHealth and notes
  3. Scale up
  4. Open a ticket with AWS
Show answer

B — Observe before changing.

Q2. Why did the failing server in scenario 4 keep getting replaced?

  1. Spot interruption
  2. ASG uses ELB health checks
  3. A bug in CloudFormation
  4. Budget limit
Show answer

B — Unhealthy targets are replaced.

Q3. What revealed the cause in scenario 10?

  1. Cost Explorer
  2. Console output showing ^M
  3. S3 logs
  4. IAM
Show answer

B — CRLF line endings in user data.

Q4. Why might the site still work when every target is unhealthy?

  1. Caching
  2. ALB fail-open
  3. DNS
  4. Luck
Show answer

B — The ALB routes to all targets.

Q5. Which tool finds a leak in another Region?

  1. Find-Waste.ps1
  2. Find-Spenders.ps1
  3. Get-Code
  4. Test-RdsAccess
Show answer

B — It sweeps every enabled Region.

Quick recap

  • You built SaatShop online from one template and kept it healthy.
  • Every scenario followed the same routine: look, ask what changed, use the layer’s tool, fix, confirm.
  • ELB health checks make Auto Scaling replace servers, which can hide or repeat a fault.
  • Console output, target health reasons and CloudTrail solved most problems.
  • Your notes are your interview stories.

Next lesson

Lesson 70: Interview and certification prep → You’ll prepare for AWS cloud engineer interviews and the CloudOps Engineer Associate and Solutions Architect Associate exams.

Further reading: AWS final project

Want to go deeper into AWS final project? Read Health checks for instances in an Auto Scaling group and What is AWS Fault Injection Service?. You can also see the full plan for this course on the AWS Cloud Administration course page.

Scroll to Top