devuplabs.cloud
Free previewLab1.5 hours

The VPC Itself

Create a custom VPC from scratch, understand CIDR and what AWS auto-creates, and compare it to the default VPC.

Prerequisites

0 of 4 checked

Create a VPC and understand what you actually built

Goal

Create a custom VPC from scratch. Understand what a VPC is and isn't. Compare it to the default VPC. Understand CIDR ranges and IP allocation.

Estimated time: 1 hour

Inspect the default VPC before touching anything

What's happening here

AWS creates a default VPC in every region automatically. It exists so you can launch things immediately without thinking about networking. The default VPC has a fixed CIDR of 172.31.0.0/16: comes with a subnet in every AZ, an attached Internet Gateway, and a route table that routes everything to the internet. This is convenient for experiments but wrong for production: every resource in the default VPC is internet-accessible by default (assuming security groups allow it), there's no isolation between workloads, and you can't customize the CIDR. We inspect it first so you know what you're comparing against.

bash
# Find the default VPC
aws ec2 describe-vpcs \
  --filters Name=isDefault,Values=true \
  --region $AWS_REGION \
  --query 'Vpcs[0].{VpcId:VpcId,CIDR:CidrBlock,Default:IsDefault,State:State,Tenancy:InstanceTenancy}'
bash
# How many subnets does it have?
DEFAULT_VPC=$(aws ec2 describe-vpcs \
  --filters Name=isDefault,Values=true \
  --region $AWS_REGION \
  --query 'Vpcs[0].VpcId' --output text)

aws ec2 describe-subnets \
  --filters Name=vpc-id,Values=$DEFAULT_VPC \
  --region $AWS_REGION \
  --query 'Subnets[*].{SubnetId:SubnetId,AZ:AvailabilityZone,CIDR:CidrBlock,Public:MapPublicIpOnLaunch}'
bash
# What's its route table look like?
aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=$DEFAULT_VPC \
  --region $AWS_REGION \
  --query 'RouteTables[*].Routes'

Observe: the default route table has a route 0.0.0.0/0 → igw-xxxxxxxx. That's why every subnet in the default VPC is internet-accessible. There's no private subnet concept here.

Checkpoint: you can see the default VPC's CIDR (172.31.0.0/16), one subnet per AZ, and the catch-all internet route.


Understand CIDR before creating your VPC

What's happening here

CIDR (Classless Inter-Domain Routing) notation like 10.0.0.0/16 encodes both a base address and a prefix length. The prefix length (/16) determines how many bits are fixed (the network) and how many are free (the hosts). /16 = 16 fixed bits → 16 free bits → 2¹⁶ = 65,536 addresses. AWS reserves 5 addresses per subnet (network address, VPC router, DNS, future use, broadcast), so a /24 subnet gives you 251 usable IPs, not 256. VPC CIDRs must be between /16 (65,536 IPs) and /28 (16 IPs). Choose your VPC CIDR carefully, you can add secondary CIDRs later but you cannot shrink or remove the primary one.

bash
# Understand what /16, /24, /28 mean in terms of IP count
python3 -c "
import ipaddress
for cidr in ['10.0.0.0/16', '10.0.0.0/24', '10.0.1.0/24', '10.0.0.0/28']:
    net = ipaddress.ip_network(cidr)
    print(f'{cidr}: {net.num_addresses} addresses, first={net.network_address}, last={net.broadcast_address}')
"
bash
# Verify non-overlapping subnets (important for VPC peering later)
python3 -c "
import ipaddress
vpc = ipaddress.ip_network('10.0.0.0/16')
subnets = list(vpc.subnets(prefixlen_diff=8))  # split into /24s
for s in subnets[:6]:
    print(s)
print(f'... total /24 subnets possible: {len(subnets)}')
"

Key rule

Plan your CIDR with future VPC peering in mind. If VPC-A is 10.0.0.0/16 and VPC-B is also 10.0.0.0/16: they cannot be peered: overlapping CIDRs are rejected. Standard practice: assign each VPC a non-overlapping /16 block (e.g. 10.0.0.0/16, 10.1.0.0/16, 10.2.0.0/16).


Create your custom VPC

What's happening here

create-vpc allocates a CIDR block and creates an empty logical boundary. At this point you have: a VPC, a default route table (local route only, 10.0.0.0/16 → local), a default network ACL (allow all), and a default security group (allow all within itself). You have no subnets, no internet gateway, no way to reach the internet, and no way to launch instances. The VPC is empty scaffolding. enableDnsSupport tells the VPC to use the AWS DNS resolver at 169.254.169.253. enableDnsHostnames gives EC2 instances in this VPC public DNS hostnames, both should be true for most workloads.

bash
export VPC_ID=$(aws ec2 create-vpc \
  --cidr-block 10.0.0.0/16 \
  --region $AWS_REGION \
  --query 'Vpc.VpcId' --output text)

echo "VPC ID: $VPC_ID"

# Tag it
aws ec2 create-tags \
  --resources $VPC_ID \
  --tags Key=Name,Value=networking-lab \
  --region $AWS_REGION

# Enable DNS support and hostnames
aws ec2 modify-vpc-attribute \
  --vpc-id $VPC_ID \
  --enable-dns-support \
  --region $AWS_REGION

aws ec2 modify-vpc-attribute \
  --vpc-id $VPC_ID \
  --enable-dns-hostnames \
  --region $AWS_REGION

echo "VPC created and DNS enabled"

Verify what was auto-created:

bash
# Default route table (local route only)
aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=$VPC_ID \
  --region $AWS_REGION \
  --query 'RouteTables[*].{Id:RouteTableId,Routes:Routes,Main:Associations[0].Main}'
bash
# Default network ACL
aws ec2 describe-network-acls \
  --filters Name=vpc-id,Values=$VPC_ID \
  --region $AWS_REGION \
  --query 'NetworkAcls[*].{Id:NetworkAclId,Default:IsDefault,Entries:Entries[*].{Rule:RuleNumber,Action:RuleAction,CIDR:CidrBlock}}'
bash
# Default security group
aws ec2 describe-security-groups \
  --filters Name=vpc-id,Values=$VPC_ID \
  --region $AWS_REGION \
  --query 'SecurityGroups[*].{Id:GroupId,Name:GroupName,Ingress:IpPermissions,Egress:IpPermissionsEgress}'

Observe: - Route table has only one route: 10.0.0.0/16 → local. No internet route. Nothing can reach outside yet. - Default NACL allows all inbound and outbound (rule 100: allow 0.0.0.0/0: rule 32767: deny all, allow wins because lower number evaluated first). - Default SG allows all traffic from sources that are also in the same SG (self-referencing). Allows all egress. No inbound from outside.

Checkpoint: VPC exists, CIDR is 10.0.0.0/16, DNS support and hostnames both enabled.

bash
aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --region $AWS_REGION \
  --query 'Vpcs[0].{VpcId:VpcId,CIDR:CidrBlock,State:State,DNS_Support:EnableDnsSupport,DNS_Hostnames:EnableDnsHostnames}'

Add a secondary CIDR block

What's happening here

You can add up to 4 secondary IPv4 CIDR blocks to a VPC without recreating it. This is used when you've exhausted your primary CIDR (run out of IPs) and need more address space, or when you need to add a specific CIDR range for VPC peering or on-premises connectivity. Secondary CIDRs must not overlap with the primary or with each other. Subnets can be carved from any associated CIDR block, primary or secondary.

bash
# Add a secondary CIDR
aws ec2 associate-vpc-cidr-block \
  --vpc-id $VPC_ID \
  --cidr-block 10.1.0.0/16 \
  --region $AWS_REGION

# Verify both CIDRs are associated
aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --region $AWS_REGION \
  --query 'Vpcs[0].CidrBlockAssociationSet[*].{CIDR:CidrBlock,State:CidrBlockState.State}'

Checkpoint: VPC now shows two associated CIDRs, 10.0.0.0/16 and 10.1.0.0/16.

bash
# Remove the secondary CIDR (we won't use it in later sessions)
SECONDARY_ASSOC=$(aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --region $AWS_REGION \
  --query 'Vpcs[0].CidrBlockAssociationSet[?CidrBlock==`10.1.0.0/16`].AssociationId' \
  --output text)

aws ec2 disassociate-vpc-cidr-block \
  --association-id $SECONDARY_ASSOC \
  --region $AWS_REGION

Understand VPC tenancy

What's happening here

VPC tenancy controls the hardware isolation of EC2 instances launched inside it. default tenancy means instances share physical hardware with other AWS customers (but remain fully isolated at the hypervisor level, this is safe). dedicated tenancy means every instance in this VPC runs on hardware dedicated solely to your account, regardless of what instance tenancy you specify at launch. Dedicated tenancy costs significantly more (dedicated host pricing) and is almost exclusively used for regulatory compliance (PCI-DSS, HIPAA) or software licensing that is tied to physical cores. You cannot change VPC tenancy after creation: this is a one-way door.

bash
# Check our VPC's tenancy
aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --region $AWS_REGION \
  --query 'Vpcs[0].InstanceTenancy'
# Expected: "default"

# For reference only, do NOT run this in the lab (creates a dedicated-tenancy VPC which costs more)
# aws ec2 create-vpc --cidr-block 10.2.0.0/16 --instance-tenancy dedicated
echo "Tenancy is 'default', correct for all non-compliance workloads"

Break It

Break 1: Try to create a VPC with an invalid CIDR

bash
# Too large: /15 is outside the allowed range
aws ec2 create-vpc \
  --cidr-block 10.0.0.0/15 \
  --region $AWS_REGION

Observe: InvalidVpc.Range error. VPC CIDRs must be between /16 and /28.

bash
# Too small: /29
aws ec2 create-vpc \
  --cidr-block 10.0.0.0/29 \
  --region $AWS_REGION

Observe: same error. A /29 has only 8 IPs (AWS reserves 5, leaving 3 usable) not enough for a VPC.

Break 2: Try to create a VPC with an overlapping CIDR (simulate the peering problem)

bash
# Create a second VPC with the same CIDR as our lab VPC
VPC_OVERLAP=$(aws ec2 create-vpc \
  --cidr-block 10.0.0.0/16 \
  --region $AWS_REGION \
  --query 'Vpc.VpcId' --output text)
echo "Overlap VPC: $VPC_OVERLAP"

# Try to peer them
aws ec2 create-vpc-peering-connection \
  --vpc-id $VPC_ID \
  --peer-vpc-id $VPC_OVERLAP \
  --region $AWS_REGION

Observe: InvalidVpcPeeringConnection.StateTransitionFailed with a message about overlapping CIDR blocks. This is the exact error you'd see in production if you didn't plan CIDRs upfront. Peering requires non-overlapping CIDRs, no exceptions.

bash
# Clean up the overlap VPC
aws ec2 delete-vpc --vpc-id $VPC_OVERLAP --region $AWS_REGION

Break 3: Try to delete the VPC while it has resources

bash
# Try to delete our lab VPC right now (it has default resources auto-created)
aws ec2 delete-vpc --vpc-id $VPC_ID --region $AWS_REGION

Observe: DependencyViolation: you cannot delete a VPC while it has dependencies (default security group, default route table, default NACL are all attached). The VPC must be fully cleaned up before deletion: subnets deleted, IGW detached and deleted, security groups deleted, etc. This is why cleanup order matters.

Compare custom VPC vs default VPC

Goal

Make the comparison concrete. Understand exactly what makes the default VPC dangerous for production use.

Estimated time: 30 minutes

bash
# Pull attributes of both VPCs side by side
echo "=== DEFAULT VPC ==="
aws ec2 describe-vpcs \
  --filters Name=isDefault,Values=true \
  --region $AWS_REGION \
  --query 'Vpcs[0].{CIDR:CidrBlock,Default:IsDefault,Tenancy:InstanceTenancy}'

echo "=== LAB VPC ==="
aws ec2 describe-vpcs \
  --vpc-ids $VPC_ID \
  --region $AWS_REGION \
  --query 'Vpcs[0].{CIDR:CidrBlock,Default:IsDefault,Tenancy:InstanceTenancy}'

# Compare route tables
echo "=== DEFAULT VPC ROUTES ==="
DEFAULT_VPC=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
  --region $AWS_REGION --query 'Vpcs[0].VpcId' --output text)
aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=$DEFAULT_VPC \
  --region $AWS_REGION \
  --query 'RouteTables[0].Routes[*].{Dest:DestinationCidrBlock,Target:GatewayId}'

echo "=== LAB VPC ROUTES ==="
aws ec2 describe-route-tables \
  --filters Name=vpc-id,Values=$VPC_ID \
  --region $AWS_REGION \
  --query 'RouteTables[0].Routes[*].{Dest:DestinationCidrBlock,Target:GatewayId}'

What you're seeing

  • Default VPC: has 0.0.0.0/0 → igw-xxx. Internet-routable from day one.
  • Lab VPC: only has 10.0.0.0/16 → local. No internet route. Nothing gets out or in until you explicitly add it (Subnets lab).

Cleanup. Keep for Subnets

Do NOT delete the VPC, it's the foundation for all subsequent networking labs. Just note the VPC ID:

bash
echo "Lab VPC ID: $VPC_ID"
# Save this, you'll need it in every subsequent layer

If you need to resume in a new session:

bash
export VPC_ID=$(aws ec2 describe-vpcs \
  --filters Name=tag:Name,Values=networking-lab \
  --region $AWS_REGION \
  --query 'Vpcs[0].VpcId' --output text)
echo "Resumed: $VPC_ID"

Unlock all 24 AWS services & 291+ lab sessions (~180 hours)

Pricing