Your network.
Your Linux server.
Build an actual Ubuntu computer in AWS. Learn how packets reach it, how SSH proves your identity, and how Terraform and Ansible rebuild the environment.
These are sample names, not our infrastructure. Before copying commands, read Customize the examples: paths, keys, IPs and names. Substitute your own authorized account and resources. Existing lab? Retain its original resource names, usernames and state; renamed samples can cause replacement. See the existing-lab warning before applying updated recipes.
A neighborhood for one computer
Terraform builds the street and house. The firewall checks who can knock. Your SSH key gets you inside. Ansible furnishes the house with Nginx, the program that answers web requests.
| Piece | Our choice | Why it matters |
|---|---|---|
| VPC / subnet | 10.42.0.0/16 / 10.42.1.0/24 | Separate disposable network; no default-VPC dependency. |
| VM | One t3.micro, Ubuntu 24.04 amd64 | Small x86 Linux learning machine. Not a guaranteed free resource. |
| Disk | 8 GiB gp3, encrypted, delete on termination | Enough for this tiny exercise; no backup of important data. |
| Inbound | TCP 22 and 80 from your IPv4 /32 | Not open to the entire internet. Other visitors cannot use the page. |
| Outbound | All IPv4 traffic allowed | Simple package downloads; intentionally broad lab policy. |
| Omitted | NAT, load balancer, Elastic IP, extra disks | Avoid extra billable services in the first exercise. |
The server has a public address, so it is not a private-subnet architecture. The security group restricts inbound access, but does not remove the need for OS patching or safe application configuration. We do not attach a cloud IAM role because this server needs no cloud API access.
AWS: public-subnet architectureSecurity groupsCanonical Ubuntu images
1. Check the bill and your permissions first
- Complete the AWS account setup and use MFA. Check your own plan, credits, expiration and service eligibility. Do not assume a “micro” VM is free or upgrade billing just to complete a tutorial.
- Use an isolated learning account and a non-root identity with authorized EC2/VPC deployment permissions. This recipe creates a VPC, subnet, route table, gateway, security group, public-key record and VM. An administrator must scope those permissions for the sandbox. It does not require creating IAM roles.
- Create a monthly account-level $5 budget with actual-spend alerts at 50%, 80% and 100%, plus forecast alerts where available. Budgets are delayed alerts, not hard caps.
- Estimate 4 running hours of t3.micro Linux in us-east-1, 8 GiB gp3 for the actual disk lifetime, one public IPv4 address for its assigned time, and a little outgoing data. Review the calculator total. Two simultaneous manual/code servers double those components.
- Set a personal timer to finish and delete the lab the same day. Stop when taking a break, but destroy when finished. Stopping does not remove disk charges, snapshots or unrelated resources.
The example uses Standard CPU credits, not Unlimited, to avoid surplus-credit charges. After burst credits run out, performance can be limited. Detailed monitoring is off; do not enable optional paid agents or security products without pricing them.
AWS calculatorEC2 pricingDisk pricingPublic IPv4 pricingBudget limitationsStandard CPU credits
2. Prepare your laptop and a key
Use Ubuntu in WSL for this lab's command-line work. Your laptop stays Windows; the cloud server runs Linux. This keeps Terraform, SSH, AWS login and Ansible in one environment rather than splitting credentials between Windows and WSL.
wsl --install -d UbuntuRestart if prompted and finish Ubuntu's first-run setup. From this point, run commands in the Ubuntu terminal, not PowerShell. Keep the project under your Ubuntu home directory, not a shared Windows folder. See Microsoft's WSL instructions.
Use your Ubuntu terminal as the control machine. Other Linux distributions can administer this server too, but the package installation commands below target Ubuntu. The managed cloud server is Ubuntu 24.04.
Install AWS CLI v2 and Terraform inside that same environment using official instructions. Prefer short-lived AWS login sessions. If your administrator provides IAM Identity Center access, configure its SSO profile:
aws configure sso --profile contoso
aws sso login --profile contoso
export AWS_PROFILE=contoso
export AWS_REGION=us-east-1
aws sts get-caller-identity
terraform versionSSO setup needs your own organization's start URL and account/role selection; it is not available just by copying a command. If you use another supported short-lived login method, follow its instructions instead. Confirm the returned account is your sandbox. Never paste access keys into this website or into Terraform files.
sudo apt update
sudo apt install -y openssh-client python3-venv
mkdir -p ~/contoso-infra
cd ~/contoso-infra
# Edit this path if you want a different dedicated key location.
LAB_SSH_KEY="$HOME/.ssh/contoso_aws"
# Stop if either file exists: never overwrite a key.
if [ -e "$LAB_SSH_KEY" ] || [ -e "$LAB_SSH_KEY.pub" ]; then
printf '%s\n' 'Key already exists. Choose a new path, or reuse it only if authorized.'
else
# Choose a passphrase. Continue only if creation and unlocking succeed.
mkdir -p "$(dirname "$LAB_SSH_KEY")" &&
ssh-keygen -t rsa -b 4096 -f "$LAB_SSH_KEY" -C contoso &&
eval "$(ssh-agent -s)" &&
ssh-add "$LAB_SSH_KEY" &&
cat "$LAB_SSH_KEY.pub" &&
realpath "$LAB_SSH_KEY.pub"
fiThe .pub file is the public half you import into AWS. The file without .pub is your private key: keep it secret. The passphrase protects that local file; the SSH agent lets Ansible use it after you unlock it. Re-run the agent setup when needed in a new terminal. Never overwrite an existing key pair.
Find the public IPv4 address used by this laptop connection (your router or VPN egress, not 192.168.x.x). Append /32, meaning one address. The value 203.0.113.10/32 below is a documentation placeholder and will not work. If you cannot get IPv4 connectivity, this particular lab needs a different authorized access design.
Your key path is your choice. $HOME expands to the home directory of the Ubuntu/WSL user running these commands; no username is hard-coded. Keep using the same LAB_SSH_KEY in this terminal. In a new terminal, set it again to your actual private-key path and unlock the key. Terraform needs the matching .pub file: replace its path placeholder with the output of realpath "$LAB_SSH_KEY.pub". Ansible needs the private file: edit both the inventory IP and its key path. A literal $LAB_SSH_KEY is not expanded inside HCL or an INI inventory.
3. Build once in the AWS console
Use us-east-1 throughout. The browser steps are the same on Windows and Linux. Names ending in manual separate this exercise from the Terraform copy.
- In the VPC console, create VPC only named
contoso-infra-manualwith IPv4 CIDR10.42.0.0/16, no IPv6. Do not choose a wizard that adds NAT gateways or paid endpoints. - Create one subnet in that VPC named
contoso-infra-manual-public, in an available zone, with CIDR10.42.1.0/24. - Create an internet gateway, attach it to this VPC, then create a route table for the VPC. Add route
0.0.0.0/0targeting that gateway. Explicitly associate the subnet with this route table. Keep the local VPC route. - Create a security group in this VPC named
contoso-infra-manual. Add inbound TCP 22 and TCP 80 with your current public IPv4/32as source for each. Keep outbound all IPv4 for this lab. Do not use “Anywhere” inbound. - In EC2 → Key pairs, import the public-key file shown by
realpath "$LAB_SSH_KEY.pub"ascontoso-infra-manual. Import the public key only. - Launch an instance named
contoso-infra-manual. Choose Canonical Ubuntu Server 24.04 LTS, amd64/x86_64, and t3.micro. Verify the official publisher, not an unrecognized marketplace image. - Select the manual key, VPC, subnet and security group. Enable an auto-assigned public IP for this instance. Choose an 8 GiB encrypted gp3 root disk, deletion on termination, and no extra disks.
- In advanced details set CPU credits to Standard, IMDSv2 tokens required, and detailed monitoring off. Leave the IAM profile and user data empty. Review the entire summary before launch; no Spot, load balancer or Elastic IP is needed.
- Wait for Running and status checks to pass. Record the instance ID, public IP, subnet and disk ID. Tag these resources
Project=contoso-infra-manualso you can find them at cleanup.
Do not create duplicate VMs when launch or checks are slow. Inspect the existing request and status first. If t3.micro is unavailable or disallowed, price and deliberately choose an alternative; the provided Terraform recipe would need updating too.
4. Verify, connect and inspect the computer
First open EC2 → select the server → Actions → Monitor and troubleshoot → Get system log. Find BEGIN SSH HOST KEY FINGERPRINTS. Compare the corresponding host-key fingerprint with the fingerprint SSH shows on first connection. This is the server's fingerprint, not your imported login key's fingerprint.
ssh -i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" ubuntu@YOUR-SERVER-IPReplace the IP. Accept the first-use key only after a match. If the log has not populated, wait and retrieve it again. If you cannot independently verify the fingerprint, stop; do not disable host-key checking or blindly accept a changed key.
sudo cloud-init status --wait
cat /etc/os-release
hostname
ip -brief address
free -h
df -h
sudo apt update
sudo apt install -y nginx
sudo systemctl enable --now nginx
curl -I http://127.0.0.1/
exitIf cloud-init reports an error, investigate before continuing. After leaving SSH, open http://YOUR-SERVER-IP/ from the permitted laptop or run the command below. Expect Nginx's default page. This page is a useful health test—the lesson is the running computer and network behind it.
curl -I http://YOUR-SERVER-IP/Your private address from ip may differ from the public IP: AWS maps the public address to the network interface. HTTP port 80 is separate from SSH port 22. A working SSH login does not mean Nginx is installed.
5. Rebuild the infrastructure with Terraform
This creates a separate code-managed lab. It does not import your manual server. To avoid paying for two copies, follow the manual cleanup section first. Store the files in ~/contoso-infra.
Save terraform.tfvars with your real current IPv4 and public-key path. The recipe rejects broad CIDRs such as /0. It does not discover your address or authenticate you automatically.
allowed_ipv4_cidr = "203.0.113.10/32"
public_key_path = "/REPLACE/WITH/YOUR/ABSOLUTE/PUBLIC-KEY-PATH.pub"aws sts get-caller-identity
terraform init
terraform fmt
terraform validate
terraform plan -out=lab.tfplan
# Read every creation and the SSH/HTTP sources before applying.
terraform apply lab.tfplan
terraform outputThe saved plan applies without a further approval prompt. Stop before apply if it shows the wrong account, unexpected services or broad inbound access. The provider uses your AWS session; no credentials belong in the recipe. Keep state, saved plans and variable files private. Keep .terraform.lock.hcl for repeatable provider selection.
The AMI lookup uses Canonical's publisher ID and the newest matching Ubuntu 24.04 image. Later, a newly published image can produce a replacement plan; do not apply that blindly. Production workflows normally pin and deliberately update their approved image.
terraform output -raw instance_id
terraform output -raw public_ip
# Verify this new instance's fingerprint in its EC2 system log first.
ssh -i "${LAB_SSH_KEY:?Set LAB_SSH_KEY to your private-key path}" ubuntu@YOUR-NEW-SERVER-IP
sudo cloud-init status --wait
exitNo Nginx is installed yet. That is intentional: Terraform owns infrastructure; Ansible will configure Ubuntu. A later terraform plan should show no unintended changes, subject to a newer AMI or manual edits.
One safe infrastructure change
If your home/VPN public IP changes, edit only allowed_ipv4_cidr, then review and apply a new plan. Expect the SSH/HTTP source rules to change. Do not open SSH to the world to troubleshoot.
6. Configure Ubuntu with Ansible
The house exists. Now give Ansible a checklist: install Nginx, put the page in the right folder, start the service and check the answer.
Run Ansible on your laptop's Ubuntu/WSL terminal, not on the remote server. Keep these three files together. Replace the inventory's IP with Terraform's public_ip output. This uses the same verified SSH connection and unlocked key.
python3 -m venv ~/.venvs/contoso-infra
source ~/.venvs/contoso-infra/bin/activate
python -m pip install ansible-core
ansible-playbook -i inventory.ini server.yml --syntax-check
ansible lab -i inventory.ini -m ansible.builtin.ping
ansible-playbook -i inventory.ini server.ymlAnsible's ping checks login and Python, not internet ICMP ping. The Ubuntu image provides Python and sudo for the ubuntu user. If it fails, check SSH first rather than changing privileges blindly. Keep host-key checking enabled.
Change the machine's configuration
Edit page_message in the playbook, then run it again. The template changes; Terraform infrastructure should not. No Nginx restart is required merely to replace a static HTML file.
ansible-playbook -i inventory.ini server.yml --check --diff
ansible-playbook -i inventory.ini server.yml
curl http://YOUR-SERVER-IP/
ansible-playbook -i inventory.ini server.ymlUse check mode after the first successful installation; it is a simulation, not proof of a successful deployment. A repeat real run should normally report no changes to packages, the template or service, though package-cache refreshes may report changes. The playbook also checks HTTP from inside the server; the external curl checks the network path.
Ansible package managementTemplatesCheck mode limitations
7. Understand failures and ongoing costs
sudo nginx -t
systemctl status nginx --no-pager
sudo journalctl -u nginx -n 30 --no-pager
sudo ss -lntp
df -h
free -h| Symptom | Check in this order |
|---|---|
| SSH times out | Running/checks → correct public IP → your current /32 → port 22 rule → subnet route and gateway → your own network/VPN. |
| Permission denied (publickey) | Username ubuntu, correct key, imported public half and instance launch key. A firewall change does not fix identity. |
| SSH host key changed | Confirm replacement or changed public address through the console and verify the new fingerprint independently. Remove only the obsolete known-host entry after verification. |
| HTTP fails, SSH works | Local curl → Nginx status/listening port → inbound port 80 source. HTTPS is not configured. |
| apt lock error | Wait for cloud-init or another legitimate package operation. Do not delete lock files to force installation. |
| Ansible cannot unlock key | Run ssh-agent and ssh-add in the same environment; confirm a normal SSH login works. |
| Server slower than expected | CPU credits, memory and disk space. Standard mode limits bursts rather than adding unlimited surplus charges. |
| Charges while stopped | Root disks, snapshots and other retained resources. Stopping is not deletion. |
Use the EC2 Monitoring tab for basic CPU/status/network metrics. RAM usage and disk filesystem fullness need in-OS checks or a separately configured monitoring agent. Budget reports may lag, so check again after cleanup.
Stopping and starting this instance normally changes its auto-assigned public IP. Refresh Terraform state with a reviewed plan, check outputs and update the inventory. This recipe does not create a stable Elastic IP.
AWS monitoringStop/start behaviorWhy stopped disks still cost money
8. Delete the lab, not just the server
The manual copy
- Confirm the exact
contoso-infra-manualinstance and terminate it, not just stop it. Wait for termination; check that its root disk was deleted. - Delete the manual security group once its network interface is gone. Disassociate and delete the manually created route table, delete the subnet, detach/delete the internet gateway, and delete the dedicated VPC. Do not touch the account's default or other VPCs.
- Delete only the imported
contoso-infra-manualpublic-key record. This does not remove your local private key. Check for extra disks, snapshots or addresses you may have added outside the instructions.
The Terraform copy
Run from the original code folder using its state and the correct authorized AWS account. Keep the local public-key file available while planning cleanup.
aws sts get-caller-identity
terraform state list
terraform plan -destroy -out=cleanup.tfplan
# Read every deletion. Stop if these are not exactly your code lab resources.
terraform apply cleanup.tfplan
terraform state list
aws sso logoutUse the appropriate session logout if you did not use SSO. Verify the instance, root disk and lab network are gone in the console. Keep state until you have confirmed cleanup; deleting a state file does not delete infrastructure. Inspect billing after reporting catches up—deletion does not erase earlier usage.
Return to infrastructure foundations · Compare with the storage-only lab