The goal
You need a Linux server on AWS for a quick test. By the end, you'll have one running, you'll be connected over SSH and you'll know how to shut it down properly.
Check your identity and region before you create anything:
aws sts get-caller-identity
aws configure get region
An instance you can't find is often one you launched in another region.
Step 1: create a key pair
The key pair is how you log in. AWS stores the public half, and you keep the private half.
aws ec2 create-key-pair \
--key-name lab-key \
--key-type ed25519 \
--query KeyMaterial \
--output text > lab-key.pem
chmod 400 lab-key.pem
AWS doesn't keep a copy of your private key. If you lose it, you'll have to use one of the recovery methods. Never commit it to Git.
On Windows PowerShell, skip chmod and restrict the file through its security properties, or ssh may refuse to use it.
Step 2: choose a network
For a lab, the default VPC works:
VPC_ID=$(aws ec2 describe-vpcs --filters Name=is-default,Values=true \
--query 'Vpcs[0].VpcId' --output text)
echo "$VPC_ID"
SUBNET_ID=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" \
--query 'Subnets[0].SubnetId' --output text)
If VPC_ID prints None, your region has no default VPC. Build a VPC first, and use its public subnet.
Step 3: allow SSH from your IP only
A security group is a firewall around the instance. By default it blocks all inbound traffic and allows all outbound.
SG_ID=$(aws ec2 create-security-group \
--group-name lab-ssh \
--description "SSH from my IP" \
--vpc-id "$VPC_ID" \
--query GroupId --output text)
MYIP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" \
--protocol tcp --port 22 \
--cidr "$MYIP/32"
Warning
Don't open port 22 to
0.0.0.0/0. Automated scanners will find it fast. For long-lived servers, skip SSH altogether and use Session Manager.
Step 4: launch the instance
AMI IDs differ by region and change over time, so don't hard-code one. AWS publishes the latest ID as a public parameter:
INSTANCE_ID=$(aws ec2 run-instances \
--image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--instance-type t3.micro \
--key-name lab-key \
--security-group-ids "$SG_ID" \
--subnet-id "$SUBNET_ID" \
--associate-public-ip-address \
--metadata-options HttpTokens=required \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=lab-01},{Key=Owner,Value=me}]' \
--query 'Instances[0].InstanceId' --output text)
aws ec2 wait instance-status-ok --instance-ids "$INSTANCE_ID"
The waiter takes a few minutes, and returns once the status checks pass. HttpTokens=required turns on IMDSv2, a safer form of instance metadata, and it's a good habit to start with.
Step 5: connect
IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" \
--query 'Reservations[0].Instances[0].PublicIpAddress' --output text)
ssh -i lab-key.pem ec2-user@"$IP"
The login user depends on the image:
| Image | User |
|---|---|
| Amazon Linux | ec2-user |
| Ubuntu | ubuntu |
| Debian | admin |
| RHEL | ec2-user |
Once connected, look around:
cat /etc/os-release
df -h /
TOKEN=$(curl -s -X PUT http://169.254.169.254/latest/api/token -H "X-aws-ec2-metadata-token-ttl-seconds: 60")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/instance-type
The last two lines read the instance's metadata using IMDSv2.
Common problems
| Problem | Likely cause | Fix |
|---|---|---|
InvalidKeyPair.NotFound | Key is in another region | Create the key in this region |
Connection timed out | Security group, your IP changed, or no route to the internet | Update the rule, and see SSH timeout |
Permission denied (publickey) | Wrong user or key | Check the table above, and use -i |
UNPROTECTED PRIVATE KEY FILE | Key file is readable by others | chmod 400 lab-key.pem |
InsufficientInstanceCapacity | No capacity in that zone | Try another type or subnet. See launch failures |
VcpuLimitExceeded | Account vCPU quota reached | Use a smaller type or request an increase |
Step 6: stop or terminate
| Action | Compute charge | Disk charge | Data on EBS |
|---|---|---|---|
| Stop | Stops | Continues | Kept |
| Start again | Resumes | Continues | Kept, but the public IP usually changes |
| Terminate | Stops | Root volume usually deleted | Usually gone |
# Stop it for now
aws ec2 stop-instances --instance-ids "$INSTANCE_ID"
# Finished with it
aws ec2 terminate-instances --instance-ids "$INSTANCE_ID"
aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"
Caution
Terminating can't be undone. By default, it also deletes the root volume. Check the instance ID and the Name tag first. For real servers, turn on termination protection with
aws ec2 modify-instance-attribute --instance-id <id> --disable-api-termination.
Now remove the rest of the lab:
aws ec2 delete-security-group --group-id "$SG_ID"
aws ec2 delete-key-pair --key-name lab-key
rm -f lab-key.pem
Before you finish, list what's still running in this region:
aws ec2 describe-instances \
--filters Name=instance-state-name,Values=pending,running,stopped \
--query 'Reservations[].Instances[].{id:InstanceId, state:State.Name, name:Tags[?Key==`Name`]|[0].Value}' \
--output table
Prevent it next time
- Tag each instance with an owner, so that you can tell what's safe to remove.
- Set a budget alert to catch forgotten instances.
- Use user data to automate setup.
- For production, use the EC2 production build guide instead of these lab settings.