Servers on EC2
Launching a machine, and what an image is.
Part 1: The problem
QuickCart’s first API server was set up by Leo, one evening, by hand. He created an SSH key, saved it on his laptop, opened port 22 to the whole internet, logged in and installed everything by typing commands.
It worked for months. Then the server had to be replaced. Nobody could remember the exact steps, and the only key was on Leo’s laptop, which had just been wiped. It took two days to make a new server that behaved like the old one.
So QuickCart wants servers that:
- can be rebuilt from scratch, by a machine, in minutes;
- need no open login port and no key file on anyone’s laptop;
- get AWS permissions without anyone storing a password or key on them.
Part 2: The idea
An EC2 instance is a computer you rent from AWS by the second. You choose what it runs, how big it is, and which network it sits in. When you don’t need it any more, you delete it and the bill stops.
What you choose when you launch one
| Choice | What it is | QuickCart’s choice |
|---|---|---|
| AMI | The starting disk: an operating system, and anything already installed | Amazon Linux 2023 |
| Instance type | How much CPU and memory | t3.micro |
| Network | Which VPC and subnet it sits in | A private app subnet (the default VPC while learning) |
| Security group | Its firewall | No inbound rules at all |
| Key pair | An SSH key for logging in | None — Session Manager instead |
| IAM role | The AWS permissions the server’s software gets | A role with Session Manager access |
| Storage | The disk, an EBS volume | 8 GiB gp3, the default |
| User data | A script that sets the server up on first boot | Installs the web server |
The AMI: the starting disk
An AMI (Amazon Machine Image) is a snapshot of a disk. Every instance starts as a copy of one. AWS publishes AMIs for common operating systems, like Amazon Linux 2023 and Ubuntu, and keeps them patched.
Teams later build their own AMIs with their software already installed. Those are often called golden images. They make new servers start faster, because there’s less to install at boot.
The instance type: how big
The name t3.micro has three parts:
| Part | Means |
|---|---|
t | The family. T is general purpose and “burstable” |
3 | The generation. Higher is newer, and usually better value |
micro | The size within the family: nano, micro, small, medium, large, xlarge and up |
| Family | Good at | Typical use |
|---|---|---|
| T, M | A balance of CPU and memory | Web servers and APIs |
| C | CPU | Encoding video, heavy calculations |
| R | Memory | Caches, in-memory data |
Burstable means a T instance runs at a low baseline and earns credits while it’s quiet. It spends them to run faster during short busy spells. That suits a small web server. It doesn’t suit something that works flat out all day: once the credits are gone, it either slows down or charges extra, depending on its credit setting.
Start small and watch the CPU graph. Choosing the smallest type that copes is called right-sizing.
User data: setting up without logging in
User data is a script you paste in when you launch. The instance runs it once, as root, on its first boot. That’s how a plain Amazon Linux AMI becomes a QuickCart server without anyone logging in.
Its output goes to /var/log/cloud-init-output.log on the server. That’s the first place to look when a new server doesn’t behave.
User data is not secret. Anyone who can view the instance’s settings can read it. Never put a password in it; Secrets covers where passwords go.
Reboot, stop and terminate
These three sound similar. They’re very different:
| Action | Instance ID | Root disk | Public IP | Compute bill | User data runs again? |
|---|---|---|---|---|---|
| Reboot | Same | Kept | Same | Continues | No |
| Stop, then start | Same | Kept | New one | Stops while stopped; the disk is still billed | No |
| Terminate | Gone | Deleted by default | Gone | Stops | — it’s gone |
Terminating is permanent. When auto scaling replaces a server, it terminates the old one and launches a new one from the AMI and user data. Anything that was only on the old disk is lost.
Getting in without SSH keys: Session Manager
The old way to reach a server is SSH: open port 22, create a key pair, and keep the private key file safe on every laptop that needs access. Keys get copied, lost and never rotated, and port 22 is scanned by bots all day.
Session Manager, part of AWS Systems Manager, gives you a shell in the browser instead:
| SSH with a key pair | Session Manager | |
|---|---|---|
| Inbound port | 22, open to someone | None |
| What proves who you are | Having the key file | Your IAM login |
| Losing access | Lose the key file | Someone removes your IAM permission |
| Record of who did what | None by default | Every session can be logged |
| Works in a private subnet | Only through a jump server | Yes, if it can reach Systems Manager |
It works because a small program on the server, the SSM agent, makes an outbound connection to AWS and waits for you. Nothing ever connects in. It needs three things:
| Needs | Why | How |
|---|---|---|
| The SSM agent | It holds the connection open | Already installed on Amazon Linux 2023 |
An IAM role with AmazonSSMManagedInstanceCore | So Systems Manager accepts the agent | The instance profile, below |
| A way out to Systems Manager on port 443 | The agent has to reach AWS | A public IP, a NAT gateway, or VPC endpoints — and an outbound security group rule |
Roles: AWS permissions without keys
QuickCart’s code on the server will need to call AWS: read a file from S3, fetch a password. The wrong way is to paste an access key into a config file. The right way is to give the server an IAM role.
You attach a role to an instance through an instance profile, a small wrapper around the role. When you create a role for EC2 in the console, it creates the instance profile for you, with the same name. From then on, the AWS SDKs and the AWS CLI on the server pick up short-lived credentials automatically, and AWS rotates them for you. Who can do what: IAM explains roles in full.
IMDSv2: protecting those credentials
The credentials come from the instance metadata service, a special address every instance can call: 169.254.169.254. It answers questions like “what’s my instance ID?” and “what are my role’s credentials?”.
| Version | How a request works | The risk |
|---|---|---|
| IMDSv1 | Any simple GET gets an answer | A bug that tricks your app into fetching a URL can fetch the credentials — the case above |
| IMDSv2 | First a PUT to get a token, then every GET must send the token | That kind of bug can rarely send a PUT with the right header |
Set Metadata version to V2 only (token required) on every instance. Amazon Linux 2023 already defaults to it, but saying so explicitly means it doesn’t depend on the AMI you pick.
Part 3: See it
Launch a server, fix something on it by hand, then reboot, stop, start and terminate it. Watch what happens to its ID, its public IP and its disk.
The instance
- State
- not launched
- Instance ID
- none
- Public IP
- none
- Compute bill
- nothing
- Disk bill
- nothing
Its disk
- No disk yet
What happened
- Nothing yet
What to notice: your hand-made fix survived a reboot and a stop, so it felt safe. It only disappeared when the server was replaced — which is exactly what auto scaling does, at 3 a.m., without asking.
Part 4: Do it
Pennies — one t3.micro for under an hour is about 2 cents in us-east-1, including its public IP address and disk. If your account is eligible for the free tier, it may cost nothing. Prices change, so check the EC2 pricing page if you’re unsure.
You’ll create a role for Session Manager, launch a server that sets itself up with user data, connect to it without any key, and see IMDSv2 refuse a request. About 30 minutes.
Check the region selector at the top right says N. Virginia (us-east-1).
Create the role for the server
In the AWS console: IAMRolesCreate role
Field Enter Trusted entity type AWS service Service or use case EC2, then the EC2 use case Choose Next. In the permissions search box, type
AmazonSSMManagedInstanceCore, tick it, and choose Next.Field Enter Role name quickcart-ec2-ssmDescription Lets QuickCart servers use Session ManagerChoose Create role.
You should seequickcart-ec2-ssmin the list of roles. Behind the scenes, the console also made an instance profile with the same name. That’s what you’ll pick when you launch.Create a security group with no way in
In the AWS console: EC2Security GroupsCreate security group
Field Enter Security group name quickcart-web-sgDescription QuickCart web server - no inbound accessVPC The default VPC Inbound rules None. Leave the list empty Outbound rules Leave the default: All traffic to 0.0.0.0/0Choose Create security group.
You should seeThe group, with 0 inbound rules and 1 outbound rule. Nothing on the internet can start a connection to a server in this group. The server can still reach out, which Session Manager needs.
Launch the instance
In the AWS console: EC2InstancesLaunch instances
Field Enter Name quickcart-web-1Application and OS Images Amazon Linux, then Amazon Linux 2023 AMI, 64-bit (x86) Instance type t3.microKey pair (login) Proceed without a key pair Under Network settings, choose Edit:
Field Enter VPC The default VPC Subnet No preference Auto-assign public IP Enable Firewall (security groups) Select existing security group, then quickcart-web-sgLeave Configure storage at its default: 8 GiB,
gp3.Open Advanced details:
Field Enter IAM instance profile quickcart-ec2-ssmMetadata version V2 only (token required) User data The script below user-data.sh#!/bin/bash dnf install -y nginx echo "QuickCart web server, set up by user data" > /usr/share/nginx/html/index.html systemctl enable --now nginxLine What it does #!/bin/bashRun this script with bash. User data must start with a line like this, or it isn’t run as a script dnf install -y nginxInstall nginx, a small web server. dnfis Amazon Linux’s package installer;-yanswers yes to its question, because nobody is there to type itecho "…" > /usr/share/nginx/html/index.htmlWrite one line of text into nginx’s home page file. >sends the text into the file instead of the screensystemctl enable --now nginxStart nginx now ( --now), and start it again on every boot (enable)Why a public IP when nothing can connect in? In the default VPC, it’s the server’s only way out to Systems Manager. In QuickCart’s real network the server sits in a private subnet and goes out through a NAT gateway or VPC endpoints instead.
Choose Launch instance.
You should seeSuccess, with a link to the instance ID. In EC2 → Instances, the state goes from Pending to Running in under a minute. Wait until Status check shows 3/3 checks passed. That can take two or three minutes.
Connect with Session Manager
In the AWS console: EC2Instancesquickcart-web-1ConnectSession Manager
Choose Connect. A terminal opens in a new browser tab. No key, no port 22.
If the Connect button is greyed out, wait a minute and reload: the agent registers itself shortly after boot.
Check who you are:
TerminalwhoamiPart What it means whoamiprint the name of the user you’re logged in as You should seessm-userSession Manager logs you in as a user called
ssm-user, which can usesudo.Check that user data ran
Read the end of the log that user data writes to:
Terminalsudo tail -n 3 /var/log/cloud-init-output.logPart What it means sudorun as root; this log is only readable by root tailprint the end of a file -n 3only the last 3 lines /var/log/cloud-init-output.logwhere the output of your user data script goes You should seeA few lines from the end of the setup, finishing with one that starts
Cloud-init v.and saysfinished at, with the date and how many seconds the boot took. Your version numbers and times will differ.Now ask nginx for its home page, from the server itself:
Terminalcurl localhostPart What it means curlmake a web request and print the answer localhostthis same machine You should seeQuickCart web server, set up by user dataNobody logged in to install nginx. The user data did it.
See IMDSv2 refuse a request without a token
Ask the metadata service for the instance’s ID the old IMDSv1 way, with no token:
Terminalcurl -s -o /dev/null -w "%{http_code}\n" http://169.254.169.254/latest/meta-data/instance-idPart What it means curlmake a web request -ssilent: don’t show a progress bar -o /dev/nullthrow the answer’s body away -w "%{http_code}\n"print only the HTTP status code, then a new line http://169.254.169.254/latest/meta-data/instance-idthe metadata service’s address, and the question: “what’s my instance ID?” You should see401401 means Unauthorized. That’s IMDSv2 doing its job: no token, no answer. Now do it properly. First, get a token:
Terminalcurl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 300"Part What it means curl -X PUTsend a PUT request, not the usual GET http://169.254.169.254/latest/api/tokenthe address that hands out tokens -H "X-aws-ec2-metadata-token-ttl-seconds: 300"a header asking for a token that lasts 300 seconds You should seeA long string of letters and numbers. It has no new line at the end, so your next prompt starts straight after it on the same line. That string is the token: select and copy it, without the prompt.
Then ask again, sending the token. Replace
TOKENwith the string you copied:Terminalcurl -H "X-aws-ec2-metadata-token: TOKEN" http://169.254.169.254/latest/meta-data/instance-idPart What it means -H "X-aws-ec2-metadata-token: TOKEN"send the token in a header http://169.254.169.254/latest/meta-data/instance-idthe same question as before You should seeYour instance ID, starting
i-, the same one the console shows. The AWS SDKs and CLI do this token dance for you automatically.Use the role, with no keys anywhere
The AWS CLI is already installed on Amazon Linux 2023. Ask AWS who this server is:
Terminalaws sts get-caller-identityPart What it means aws ststhe AWS service that deals with identities and credentials get-caller-identity“who am I?” It needs no special permission You should seeA short JSON answer. The
Arnline containsassumed-role/quickcart-ec2-ssm/followed by the instance ID. The server is acting as the role you gave it. You never typed a key: the CLI fetched short-lived credentials from the metadata service, using a token.Close the Session Manager tab when you’re done.
Stop and start it, and watch the IP change
In the AWS console: EC2Instancesquickcart-web-1Details
Write down the Public IPv4 address. Then choose Instance state → Stop instance, and confirm with Stop. When the state reads Stopped, choose Instance state → Start instance.
You should seeThe same instance ID, but a different public IPv4 address. Connect with Session Manager again and run
curl localhost: the page is still there, because the disk was kept.That changing address is why nothing should ever point straight at a server. The next lesson, Load balancers, puts one fixed address in front.
Clean-up step: Clean up
In the AWS console: EC2Instancesquickcart-web-1Instance stateTerminate instance
Newer consoles label it Terminate (delete) instance. Confirm it. The instance goes to Shutting-down, then Terminated, and its disk is deleted with it.
Then delete the security group:
In the AWS console: EC2Security Groupsquickcart-web-sgActionsDelete security groups
If it refuses because the group is still in use, wait until the instance shows Terminated and try again.
Keep the
quickcart-ec2-ssmrole. IAM roles are free, and the next two lessons use it. If you aren’t carrying on, delete it in IAM → Roles.You should seeThe instance listed as Terminated (it disappears from the list after about an hour) and no
quickcart-web-sgin the security groups. Nothing is left running.
Part 5: Check yourself
You fixed a config file by hand on a server. Later, auto scaling replaces that server. What happens to your fix?
What is user data?
Why does QuickCart use Session Manager instead of SSH keys?
An app on the server has a bug that lets an attacker make it fetch any URL. What stops them reading the server's role credentials from the metadata service?
You stop an instance and start it again the next day. What has changed?
Part 6: Cheat sheet
| Term | In one line |
|---|---|
| Instance | A computer rented by the second |
| AMI | The starting disk every instance is copied from |
t3.micro | Family t, generation 3, size micro |
| User data | A script run once, as root, on first boot. Not secret |
/var/log/cloud-init-output.log | What your user data printed. Look here first |
| Reboot | Same ID, same disk, same IP |
| Stop, then start | Same ID, same disk, new public IP |
| Terminate | Gone, disk and all |
| Session Manager | A shell with no open port and no key. Needs the agent, a role, and a way out on 443 |
| Instance profile | How a role is attached to an instance |
AmazonSSMManagedInstanceCore | The policy Session Manager needs |
| IMDSv2 | Metadata needs a token first. Set V2 only |
| Golden rule | Never fix a server by hand. Change the AMI or user data |