AWS for DevOps — Practice Projects: Exercises
Practice Exercises
Test your understanding of the five practice projects with these hands-on challenges.
Exercise 1 — VPC Design Challenge
Scenario: Your company needs a VPC that supports three environments (dev, staging, prod) with the following requirements:
- All environments share one VPC
- Each environment needs public and private subnets
- Environments must be isolated from each other using NACLs
- Only the prod environment needs a NAT Gateway
Task: Design the CIDR allocation scheme and write the AWS CLI commands.
Show Answer
CIDR Allocation:
| Environment | Public Subnets | Private Subnets |
|---|---|---|
| Dev | 10.0.0.0/24, 10.0.1.0/24 | 10.0.10.0/24, 10.0.11.0/24 |
| Staging | 10.0.2.0/24, 10.0.3.0/24 | 10.0.12.0/24, 10.0.13.0/24 |
| Prod | 10.0.4.0/24, 10.0.5.0/24 | 10.0.14.0/24, 10.0.15.0/24 |
# Create VPC
$ aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=multi-env-vpc}]'
# Create NACL for dev isolation
$ aws ec2 create-network-acl \
--vpc-id vpc-xxx \
--tag-specifications 'ResourceType=network-acl,Tags=[{Key=Name,Value=dev-nacl}]'
# Allow only dev CIDR ranges in dev NACL
$ aws ec2 create-network-acl-entry \
--network-acl-id acl-dev \
--rule-number 100 --protocol -1 \
--cidr-block 10.0.0.0/22 \
--rule-action allow --ingress
# Deny staging and prod ranges
$ aws ec2 create-network-acl-entry \
--network-acl-id acl-dev \
--rule-number 200 --protocol -1 \
--cidr-block 10.0.2.0/22 \
--rule-action deny --ingress
Key Insight: Using /22 blocks for each environment gives room for growth while maintaining clear isolation boundaries.
Exercise 2 — Blue/Green ECS Deployment
Scenario: Your ECS Fargate service needs zero-downtime deployments using a blue/green strategy instead of rolling updates.
Task: Modify the ECS service from Project 2 to use CodeDeploy blue/green deployments with two target groups.
Show Answer
# Create a second target group (green)
$ aws elbv2 create-target-group \
--name fargate-app-tg-green \
--protocol HTTP --port 3000 \
--vpc-id vpc-xxx --target-type ip \
--health-check-path "/"
# Create CodeDeploy application
$ aws deploy create-application \
--application-name ecs-fargate-app \
--compute-platform ECS
# Create deployment group
$ aws deploy create-deployment-group \
--application-name ecs-fargate-app \
--deployment-group-name prod \
--service-role-arn arn:aws:iam::123456789012:role/CodeDeployECSRole \
--deployment-config-name CodeDeployDefault.ECSLinear10PercentEvery1Minutes \
--ecs-services clusterName=prod-cluster,serviceName=fargate-app \
--load-balancer-info targetGroupPairInfoList="[{targetGroups=[{name=fargate-app-tg},{name=fargate-app-tg-green}],prodTrafficRoute={listenerArns=[arn:aws:elasticloadbalancing:...:listener/app/prod-alb/abc123/def456]}}]" \
--blue-green-deployment-configuration '{
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 5
},
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 0
}
}'
Flow: New version deploys to green target group → traffic shifts gradually (10% per minute) → old blue tasks terminate after 5 minutes.
Exercise 3 — Add DynamoDB Streams to Serverless API
Scenario: You need to add real-time data processing to the Serverless API from Project 3. Every time an item is created or updated, a Lambda function should process the change and send a notification.
Task: Enable DynamoDB Streams and create a stream processing Lambda.
Show Answer
# Enable DynamoDB Streams
$ aws dynamodb update-table \
--table-name Items \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES
{
"TableDescription": {
"LatestStreamArn": "arn:aws:dynamodb:us-east-1:123456789012:table/Items/stream/2026-03-30T06:00:00.000"
}
}
# Create stream processor Lambda
$ cat > stream-processor.py << 'EOF'
import json
import boto3
sns = boto3.client('sns')
def handler(event, context):
for record in event['Records']:
event_name = record['eventName'] # INSERT, MODIFY, REMOVE
new_image = record.get('dynamodb', {}).get('NewImage', {})
if event_name == 'INSERT':
item_name = new_image.get('name', {}).get('S', 'Unknown')
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789012:info-alerts',
Subject=f'New item created: {item_name}',
Message=json.dumps(record, default=str)
)
return {'statusCode': 200}
EOF
# Deploy and create event source mapping
$ zip stream-processor.zip stream-processor.py
$ aws lambda create-function \
--function-name stream-processor \
--runtime python3.12 \
--handler stream-processor.handler \
--role arn:aws:iam::123456789012:role/lambda-api-role \
--zip-file fileb://stream-processor.zip
$ aws lambda create-event-source-mapping \
--function-name stream-processor \
--event-source-arn arn:aws:dynamodb:us-east-1:123456789012:table/Items/stream/2026-03-30T06:00:00.000 \
--starting-position LATEST \
--batch-size 10
Result: Every item change in DynamoDB automatically triggers the stream processor Lambda.
Exercise 4 — Add Nested Stacks to CloudFormation
Scenario: The single CloudFormation template from Project 4 is getting large. Refactor it into nested stacks for better maintainability.
Task: Split the template into: networking stack, security stack, compute stack, and database stack.
Show Answer
Parent stack (main.yaml):
AWSTemplateFormatVersion: '2010-09-09'
Description: Parent stack - 3-Tier Application
Parameters:
EnvironmentName:
Type: String
Default: prod
TemplatesBucket:
Type: String
Description: S3 bucket containing nested templates
Resources:
NetworkingStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Sub 'https://${TemplatesBucket}.s3.amazonaws.com/networking.yaml'
Parameters:
EnvironmentName: !Ref EnvironmentName
SecurityStack:
Type: AWS::CloudFormation::Stack
DependsOn: NetworkingStack
Properties:
TemplateURL: !Sub 'https://${TemplatesBucket}.s3.amazonaws.com/security.yaml'
Parameters:
VpcId: !GetAtt NetworkingStack.Outputs.VPCId
ComputeStack:
Type: AWS::CloudFormation::Stack
DependsOn: SecurityStack
Properties:
TemplateURL: !Sub 'https://${TemplatesBucket}.s3.amazonaws.com/compute.yaml'
Parameters:
PrivateSubnets: !GetAtt NetworkingStack.Outputs.PrivateSubnets
ALBSecurityGroup: !GetAtt SecurityStack.Outputs.ALBSecurityGroupId
DatabaseStack:
Type: AWS::CloudFormation::Stack
DependsOn: SecurityStack
Properties:
TemplateURL: !Sub 'https://${TemplatesBucket}.s3.amazonaws.com/database.yaml'
Parameters:
DBSubnets: !GetAtt NetworkingStack.Outputs.DBSubnets
DBSecurityGroup: !GetAtt SecurityStack.Outputs.DBSecurityGroupId
# Upload templates to S3
$ aws s3 mb s3://cfn-templates-123456
$ aws s3 cp networking.yaml s3://cfn-templates-123456/
$ aws s3 cp security.yaml s3://cfn-templates-123456/
$ aws s3 cp compute.yaml s3://cfn-templates-123456/
$ aws s3 cp database.yaml s3://cfn-templates-123456/
# Deploy parent stack
$ aws cloudformation create-stack \
--stack-name prod-3tier-nested \
--template-body file://main.yaml \
--parameters ParameterKey=TemplatesBucket,ParameterValue=cfn-templates-123456 \
--capabilities CAPABILITY_NAMED_IAM
Benefits: Each team can own their stack, templates are reusable, and changes are isolated.
Exercise 5 — Composite Alarm with Auto-Scaling
Scenario: Create a composite CloudWatch alarm that only triggers when BOTH CPU is high AND memory is high (indicating a real resource constraint, not just a CPU spike from a batch job).
Task: Create individual alarms and combine them into a composite alarm that triggers auto-scaling.
Show Answer
# Individual CPU alarm (don't attach action — let composite handle it)
$ aws cloudwatch put-metric-alarm \
--alarm-name "CPU-High-Component" \
--namespace AWS/EC2 \
--metric-name CPUUtilization \
--statistic Average --period 300 \
--threshold 80 --comparison-operator GreaterThanThreshold \
--evaluation-periods 2
# Individual Memory alarm (requires CloudWatch Agent)
$ aws cloudwatch put-metric-alarm \
--alarm-name "Memory-High-Component" \
--namespace CWAgent \
--metric-name mem_used_percent \
--statistic Average --period 300 \
--threshold 80 --comparison-operator GreaterThanThreshold \
--evaluation-periods 2
# Composite alarm — BOTH must be in ALARM state
$ aws cloudwatch put-composite-alarm \
--alarm-name "Resource-Constrained-Composite" \
--alarm-rule 'ALARM("CPU-High-Component") AND ALARM("Memory-High-Component")' \
--alarm-actions arn:aws:sns:us-east-1:123456789012:critical-alerts \
--alarm-description "Both CPU and memory are high — genuine resource constraint"
# Add auto-scaling action
$ aws autoscaling put-scaling-policy \
--auto-scaling-group-name prod-asg \
--policy-name scale-on-resource-constraint \
--policy-type StepScaling \
--adjustment-type ChangeInCapacity \
--step-adjustments MetricIntervalLowerBound=0,ScalingAdjustment=2
Why Composite? A CPU spike during a batch job shouldn’t trigger scaling. But if both CPU and memory are consistently high, you have a genuine capacity problem.
Bonus Challenge
Combine all five projects into a single, fully automated deployment:
- Use CloudFormation (Project 4) to create the VPC (Project 1)
- Deploy the ECS service (Project 2) inside that VPC
- Add a serverless microservice (Project 3) alongside it
- Monitor everything (Project 5)
The goal: One aws cloudformation create-stack command deploys your entire production environment.
Key Takeaways
| Project | Core Lesson |
|---|---|
| VPC | Network design is foundational — get it right first |
| ECS CI/CD | Automation eliminates human error in deployments |
| Serverless | Pay for value, not idle capacity |
| CloudFormation | Infrastructure as Code makes environments reproducible |
| Monitoring | You can’t fix what you can’t see |
Next Chapter → Next Steps