We closed the previous module with MercadoFresco ready to go: a secured account, a chosen region, a
console we know our way around and a CLI configured with the mercadofresco-dev profile. All of that
was scaffolding. Now the building work starts. In this lesson we bring up MercadoFresco's first
real server in the cloud with Amazon EC2 (Elastic Compute Cloud), the service that provides
virtual machines on demand and the piece much of what follows rests on.
EC2 is the service almost everyone enters AWS through, and also the one where the most money is
thrown away through carelessness. That is why we are not going to take a stroll through the launch
screen: we are going to understand what an instance really is, how to choose its size without
guessing, what CPU credits are (they will be decisive for the Friday peak), how to automate the
server's start-up with user data, and how an Auto Scaling group turns MercadoFresco's problem 1
—the Friday-evening outages— into a problem solved by configuration.
Contents
- What an EC2 instance is and what problem it solves
- AMI: the template an instance is born from
- Instance families and types: how to read
t3.micro - How to choose the size without guessing
- Burstable instances and CPU credits
- Purchase models: on demand, spot, reserved and Savings Plans
- Instance lifecycle: stopping is not terminating
- Accessing the instance: key pairs, SSH, Instance Connect and Session Manager
user data: letting the instance install itself- Instance metadata and IMDSv2
- Creating the instance in the console, step by step
- Creating the instance from the CLI with the tagging scheme
- Launch templates and Auto Scaling: the answer to the Friday peak
- Shutting everything down and deleting it so you do not spend
What an EC2 instance is and what problem it solves
An EC2 instance is a virtual machine that runs on AWS's physical infrastructure. It has CPU, memory, network, a boot disk and a complete operating system: as far as your software is concerned it is indistinguishable from a real server. You can install whatever you like, open an SSH session, look at the logs and reboot it.
What makes it different from the server in MercadoFresco's office is not what the machine is, but how it is obtained and given back:
| Physical office server | EC2 instance | |
|---|---|---|
| Provisioning time | Weeks (purchase, shipping, installation) | Under a minute |
| Up-front cost | €6,000 paid in one go | €0 |
| Ongoing cost | Electricity, maintenance, spare parts | Per second of use |
| Changing size | Buy RAM and open the case | Stop, change the type, start |
| Having 10 identical ones | Buy 10 servers | One API call |
| Giving it back | Sell it second-hand | terminate-instances |
That last row is the conceptual key to the whole module: in AWS, creating and destroying are symmetrical, cheap operations. When Marta needs four storefront servers on Friday at 17:00 and only one on Saturday morning, that stops being a fantasy and turns into a single line of configuration.
A note on what EC2 does not cover in this lesson. Every instance lives inside a virtual network (VPC) and is protected by a security group, which is its firewall. Here we will use the default VPC and create a minimal security group without going into detail: networking is the whole of module 3 (03-01 VPC, 03-02 security groups and NACLs). The instance's disk is managed with EBS, which is lesson 02-02.
AMI: the template an instance is born from
An AMI (Amazon Machine Image) is a frozen disk image: operating system, installed packages, configuration and files. When you launch an instance, AWS copies the AMI onto a new volume and boots the machine from it. The AMI is the template; the instance is the living copy.
There are four possible sources:
| Source | What it is | When to use it |
|---|---|---|
| AWS AMI | Amazon Linux 2023, Ubuntu, Windows Server, Debian… maintained and patched | Usual starting point |
| AWS Marketplace | Third-party images (sometimes with an added hourly cost) | Pre-installed commercial software |
| Community AMI | Published by any user | With caution: not audited |
| Your own AMI | Created by you from an instance you have configured | Fast, reproducible start-ups |
For MercadoFresco we will use Amazon Linux 2023: it is free, it is optimised for EC2, it ships with the AWS CLI v2 pre-installed and the Systems Manager agent already active (important for Session Manager, further down).
One detail that confuses people at first: an AMI's identifier is different in every region. The
same Amazon Linux 2023 has one ami-0abc… in eu-west-1 and a different one in eu-central-1. That
is why you never write it by hand in a script; you look it up. The robust way is to ask AWS's public
parameter store:
# Gets the ID of the most recent Amazon Linux 2023 (x86_64) AMI in eu-west-1.
# The parameter is public: AWS updates it every time it publishes a new image.
aws ssm get-parameter \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameter.Value' \
--output text \
--profile mercadofresco-dev \
--region eu-west-1Output (an example; yours will be different):
Breakdown of the command:
aws ssm get-parameter: Systems Manager Parameter Store holds key/value pairs. AWS publishes the IDs of its AMIs there so that you do not have to hunt for them.--query 'Parameter.Value': JMESPath, just as we saw in 01-05, to keep only the value.--output text: no quotes, ready to drop straight into a shell variable.
Save it in a variable, because we will use it several times:
AMI_ID=$(aws ssm get-parameter \
--name /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
--query 'Parameter.Value' --output text \
--profile mercadofresco-dev --region eu-west-1)
echo "Selected AMI: $AMI_ID"Instance families and types: how to read t3.micro
The instance type defines how much CPU, memory, network and disk the machine has. AWS offers hundreds of them, but the name is a readable code that you decode piece by piece:
t3.micro │││ └──── size within the family ││└─────── (optional) extra attributes: a = AMD, g = Graviton/ARM, d = local disk, n = enhanced networking │└──────── generation (3 = third; the higher the number, the more modern and usually better price/performance) └───────── family: what it is optimised for
Another example: m6g.large = family m (balanced), generation 6, g for Graviton (AWS's
ARM processor), size large.
The families you need to know:
| Family | Optimised for | Approx. vCPU:RAM ratio | Example of use at MercadoFresco |
|---|---|---|---|
| T (t3, t4g) | General purpose, burstable, cheap | 1:2 / 1:4 | Luis's development environment, storefront server with irregular traffic |
| M (m6i, m7g) | Balanced general purpose, sustained performance | 1:4 | Storefront server in production |
| C (c6i, c7g) | Compute intensive (lots of CPU) | 1:2 | Catalogue image processing, delivery route calculation |
| R (r6i, r7g) | Memory intensive | 1:8 | Large caches, Sara's in-memory analytical reports |
| G / P | GPU (graphics, machine learning) | Variable | Not applicable at MercadoFresco today |
| I / D | Fast local storage | Variable | Databases with local NVMe |
And the sizes, which as a rule double resources and price at every step:
| Size | vCPU | RAM (t3 family) | Relative price |
|---|---|---|---|
nano |
2 (burst) | 0.5 GiB | 1× |
micro |
2 (burst) | 1 GiB | 2× |
small |
2 (burst) | 2 GiB | 4× |
medium |
2 (burst) | 4 GiB | 8× |
large |
2 | 8 GiB | 16× |
xlarge |
4 | 16 GiB | 32× |
2xlarge |
8 | 32 GiB | 64× |
This linearity has an important and counter-intuitive consequence: two large instances cost the
same as one xlarge, but two instances survive the loss of an Availability Zone and one xlarge
does not. It is the first argument for the multi-AZ design we saw in 01-03, and one of the reasons
why we will prefer to scale horizontally (more machines) before scaling vertically (bigger
machines).
How to choose the size without guessing
The professional method is not to guess, it is to measure. For MercadoFresco:
- Start from the figure you already know. One storefront instance handles 600 orders/hour. The Friday peak is 900 orders/hour. With a single machine, 33 % of the peak's orders are lost: those are the outages of problem 1.
- Start small. In the cloud, changing size costs you a reboot. Starting big "just in case" costs money every hour, forever.
- Measure with CloudWatch (lesson 05-01): CPU, memory (needs the agent), network and latency for at least one full week, including a Friday.
- Apply the 40-60 % rule. If average CPU is below 40 %, the machine is oversized. If it stays above 70 % for long periods, it is undersized.
- Check Compute Optimizer, the free AWS service that analyses your metrics and recommends a type and a size.
For our starting point we choose t3.micro for two reasons: it falls inside the Free Tier (750
hours a month for 12 months) and its burstable behaviour lets us explain CPU credits, which are
exactly what trips up so many people during their first traffic spike.
Burstable instances and CPU credits
Here is the concept that causes newcomers the most grief, and the one that matters most for MercadoFresco's Friday.
Instances in the T family do not give you the whole CPU all the time. They give you a baseline —a percentage of the core— and accumulate CPU credits while you consume less than that baseline. When you need more, you spend credits to reach 100 %. If you run out, the instance is throttled and drops abruptly to the baseline, even though the physical CPU is sitting idle.
| Type | vCPU | Baseline per vCPU | Credits earned/hour | Max. credits accruable |
|---|---|---|---|---|
t3.nano |
2 | 5 % | 6 | 144 |
t3.micro |
2 | 10 % | 12 | 288 |
t3.small |
2 | 20 % | 24 | 576 |
t3.medium |
2 | 20 % | 24 | 576 |
t3.large |
2 | 30 % | 36 | 864 |
One credit = one minute of one vCPU at 100 %.
Let us do MercadoFresco's real calculation. A t3.micro accrues 12 credits/hour, with a maximum of
288 (the equivalent of 24 hours of accrual). The Friday peak lasts from 17:00 to 21:00, four
hours, and during that time the storefront would be at 100 % CPU on its 2 vCPUs:
Consumption during the peak = 2 vCPU × 100 % × 60 min × 4 h = 480 credits Credits available = 288 accrued + (12/h × 4 h) = 336 credits Deficit = 480 − 336 = 144 credits
In other words: the t3.micro lasts under three hours of the peak and then throttles to 10 % CPU.
The storefront does not go down for lack of server, it goes down for lack of credits, and on the
CloudWatch dashboard the CPU looks flat and low, which is very misleading if you do not know why.
There are two ways out, and it is worth understanding that only one of them is good:
unlimitedmode (enabled by default on T3): when the credits run out, AWS lets you carry on at 100 % and charges you extra for every surplus vCPU-hour. It avoids the outage, but the bill goes through the roof if the peak is a regular event. You can forcestandardmode so that it never overcharges, in exchange for accepting the throttling.- Scale horizontally: add instances during the peak. This is the right answer, and we will see it at the end of this lesson with Auto Scaling.
Checking the credit balance (the CPUCreditBalance metric, in CloudWatch):
aws cloudwatch get-metric-statistics \
--namespace AWS/EC2 \
--metric-name CPUCreditBalance \
--dimensions Name=InstanceId,Value=i-0123456789abcdef0 \
--start-time 2026-08-01T00:00:00Z \
--end-time 2026-08-02T00:00:00Z \
--period 3600 \
--statistics Average \
--profile mercadofresco-dev --region eu-west-1Rule of thumb. T instances are excellent for irregular workloads with long troughs (development, test environments, internal services). For a sustained, predictable load, an M works out cheaper and more predictable than a T in
unlimitedmode.
Purchase models: on demand, spot, reserved and Savings Plans
The same instance can cost four different prices depending on the commitment you take on. Here we only compare them so that you know they exist and when each one is used; the full economic analysis, with payback calculations, is lesson 11-05.
| Model | Typical discount | Commitment | Can be interrupted | Use at MercadoFresco |
|---|---|---|---|---|
| On demand | 0 % (base price) | None | No | Everything we do in the course; unpredictable spikes |
| Spot | Up to 90 % | None | Yes, with 2 min notice | Overnight catalogue image processing |
| Reserved (RI) | Up to 72 % | 1 or 3 years, specific type | No | Fixed baseline of the storefront once it settles |
| Savings Plans | Up to 72 % | 1 or 3 years, spend/hour, not type | No | Same as RI but with family flexibility |
| Dedicated host | — (more expensive) | Variable | No | Licences that require your own physical hardware |
Three ideas to take away:
- Spot is not "cheap and worse": it is exactly the same machine, with the condition that AWS can take it back. It only suits workloads that tolerate being interrupted and retried.
- Reserved instances and Savings Plans are not machines, they are billing discounts applied automatically to consumption you already have.
- Do not buy commitments until you have three months of real data. Marta will not reserve anything until module 11.
Instance lifecycle: stopping is not terminating
An instance moves through well-defined states, and confusing two of them —stopped and terminated—
is the most expensive and most irreversible mistake in EC2.
stateDiagram-v2
[*] --> pending: run-instances
pending --> running: start-up completed
running --> stopping: stop-instances
stopping --> stopped: powered off
stopped --> pending: start-instances
running --> shutting_down: terminate-instances
stopped --> shutting_down: terminate-instances
shutting_down --> terminated: resources released
terminated --> [*]
running --> rebooting: reboot-instances
rebooting --> running: same host, same disk
What happens exactly at each transition:
| Action | Is the instance billed? | Is the root disk kept? | Does the public IP change? | Reversible? |
|---|---|---|---|---|
Reboot (reboot) |
Yes (it never stopped) | Yes | No | — |
Stop (stop) |
No (the EBS disk is) | Yes | Yes, the automatic public IP is lost | Yes |
| Hibernate | No | Yes, including the RAM dumped to disk | Yes | Yes |
Terminate (terminate) |
No | No, deleted by default | — | No, never |
The four nuances to memorise:
- Stopping deletes nothing. The root EBS volume still exists and still costs money (around 0.08 USD per GB and month with gp3). A stopped instance is almost free, but not quite.
- Terminating is final. There is no recycle bin. The root volume is deleted unless you have
disabled
DeleteOnTermination. If you were keeping data there, it has gone. - The automatic public IP is lost when you stop. When you start again you get a different one. If you need a stable IP, use an Elastic IP (module 3) or, better, a DNS name (03-05).
- The private IP is kept for as long as the instance exists.
For production, Marta will always enable termination protection:
# Stops an accidental terminate-instances from destroying the instance.
aws ec2 modify-instance-attribute \
--instance-id i-0123456789abcdef0 \
--disable-api-termination \
--profile mercadofresco-dev --region eu-west-1To allow deletion again you have to run the command with --no-disable-api-termination. It is
deliberate friction: two conscious steps instead of one irreversible click.
Accessing the instance: key pairs, SSH, Instance Connect and Session Manager
A key pair is a pair of cryptographic keys. AWS keeps the public one and places it inside the
instance at start-up (in ~/.ssh/authorized_keys); you keep the private one in a .pem file.
AWS does not keep a copy of the private key: if you lose it, there is no recovery possible
through support.
# Creates the key pair and saves the private key with the right permissions.
aws ec2 create-key-pair \
--key-name mercadofresco-tienda \
--key-type ed25519 \
--query 'KeyMaterial' --output text \
--profile mercadofresco-dev --region eu-west-1 \
> ~/.ssh/mercadofresco-tienda.pem
# Without this chmod, the SSH client refuses the key because other users can read it.
chmod 400 ~/.ssh/mercadofresco-tienda.pemNotes on the command:
--key-type ed25519is more modern and shorter than RSA; both work, but ed25519 is today's default choice (Windows with older instances may requirersa).- The
KeyMaterialoutput is shown only once. If you do not redirect it to a file, it is gone.
Connecting, once the instance is running:
The default user depends on the AMI: ec2-user on Amazon Linux, ubuntu on Ubuntu, admin on
Debian. Logging in as root is disabled on purpose.
There are two alternatives that avoid handling .pem files, and they are worth knowing:
| Method | Needs a .pem key |
Needs port 22 open | Audit trail | Comment |
|---|---|---|---|---|
| Classic SSH | Yes | Yes, from your IP | No, unless you build it | Universal, always works |
| EC2 Instance Connect | No | Yes (from AWS ranges) | Yes, in CloudTrail | The console's "Connect" button; injects a temporary 60 s key |
| Session Manager | No | No, no port open at all | Yes, complete, with session recording | Needs the SSM agent and an IAM role on the instance |
Session Manager (part of AWS Systems Manager) is the option MercadoFresco will use in production: the instance needs neither a public IP nor an open SSH port, because it is the one that starts the outbound connection towards AWS. Less attack surface, and a complete audit trail of who got in and what they typed.
# With the Session Manager plugin installed on your machine:
aws ssm start-session \
--target i-0123456789abcdef0 \
--profile mercadofresco-dev --region eu-west-1The IAM permissions this requires are covered in lesson 04-01; the security groups that control port 22, in 03-02.
user data: letting the instance install itself
The user data field is a script the instance runs as root, exactly once, on its first
start-up. It is the difference between "I have created a server" and "I have created a server that
is already serving the storefront".
This is MercadoFresco's script. Save it as user-data-tienda.sh:
#!/bin/bash
set -euxo pipefail
# set -e : abort on the first command that fails
# set -u : error if an undefined variable is used
# set -x : trace every command into the log (essential for debugging afterwards)
# pipefail: a failure in the middle of a pipeline is not masked
# 1. Update the system and record the start-up timestamp
dnf update -y
echo "MercadoFresco instance start-up: $(date -Is)" >> /var/log/mercadofresco-arranque.log
# 2. Install the web server and PHP (the MercadoFresco monolith is PHP)
dnf install -y nginx php-fpm php-pgsql
# 3. Retrieve the instance's own metadata using IMDSv2 (see the next section)
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 300")
INSTANCE_ID=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id)
AZ=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/placement/availability-zone)
# 4. Provisional home page that identifies which instance is answering.
# This is essential once we have several behind a load balancer (lesson 03-03):
# reloading the page shows which one you landed on.
cat > /usr/share/nginx/html/index.html <<HTML
<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>MercadoFresco</title></head>
<body style="font-family:system-ui;max-width:40rem;margin:4rem auto">
<h1>MercadoFresco</h1>
<p>Fresh produce in 24 hours.</p>
<hr>
<p><strong>Instance:</strong> ${INSTANCE_ID}</p>
<p><strong>Availability Zone:</strong> ${AZ}</p>
</body>
</html>
HTML
# 5. Health check endpoint for the load balancer and for Auto Scaling.
# It must return 200 and be cheap: no database queries in here.
echo "OK" > /usr/share/nginx/html/salud
# 6. Start the services and leave them enabled for future reboots
systemctl enable --now nginx php-fpmHow it behaves and how to debug it:
- It runs only on the first start-up. If you stop and start the instance, it is not repeated.
- The complete output is left in
/var/log/cloud-init-output.loginside the instance. When something "does not work and I do not know why", that file is the first place to look. - It must be idempotent and non-interactive: no
apt installwithout-y, no waiting for a keypress. - Never put credentials in
user data: anyone with access to the instance can read it with acurlto the metadata. For secrets you use Secrets Manager (lesson 04-03).
Instance metadata and IMDSv2
Every instance can ask itself who it is, by querying a special address that only answers from the
inside: 169.254.169.254. That is where the instance metadata lives: its ID, its type, its
AZ, its IPs, its tags (if you enable them) and —very importantly— the temporary credentials of the
associated IAM role.
That last part explains why the security of this endpoint matters so much. Version 1 (IMDSv1)
answered any GET, which meant an SSRF-type vulnerability in the web application could make the
server leak its own credentials. IMDSv2 requires you to obtain a token first with a PUT,
something a simple SSRF cannot do.
# Step 1: request the token (mandatory in IMDSv2). TTL in seconds.
TOKEN=$(curl -sX PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# Step 2: use the token on every query
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-type
# List every available path
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/Paths that are useful day to day:
| Path | Returns |
|---|---|
instance-id |
i-0123456789abcdef0 |
instance-type |
t3.micro |
placement/availability-zone |
eu-west-1a |
placement/region |
eu-west-1 |
local-ipv4 / public-ipv4 |
Private / public IP |
iam/security-credentials/<role> |
Temporary credentials of the role |
spot/instance-action |
Notice that a spot instance is being reclaimed |
MercadoFresco policy: always require IMDSv2. It is enforced when launching the instance with
--metadata-options "HttpTokens=required", as we will do in the creation command.
Creating the instance in the console, step by step
We will take the visual route first, because it teaches the vocabulary, and then get the same result from the CLI, which is the one you automate.
- Console → search for EC2 → check in the top right that the region is Ireland
(
eu-west-1). This check is not optional: it is the trap we saw in 01-04. - Instances → Launch instances.
- Name and tags:
mercadofresco-tienda-01. Click Add additional tags and fill in the project's mandatory scheme:Proyecto = mercadofrescoEntorno = desarrolloComponente = tiendaPropietario = luisCentroCoste = operaciones
- Image (AMI): Amazon Linux 2023,
x86_64architecture. Note the Free tier eligible label. - Instance type:
t3.micro. - Key pair: select
mercadofresco-tienda(the one you created earlier) or create it here and download the.pem. - Network settings: leave the default VPC and create a security group called
sg-mercadofresco-tiendawith two inbound rules: HTTP (80) from 0.0.0.0/0 and SSH (22) from My IP. Never SSH open to the world. The detail of this is lesson 03-02. - Storage: 8 GiB
gp3, the default value. Disks are lesson 02-02. - Advanced details → scroll to the bottom and paste the contents of
user-data-tienda.shinto the User data field. In the same block, check that IMDS version is set to V2 only. - Review the summary panel on the right and click Launch instance.
In 30-60 seconds the state will change to running and the status checks (2/2) will turn green. Copy
the public IP and open it in your browser: you will see the MercadoFresco page with the instance
ID and its Availability Zone.
Cost warning. A
t3.microis inside the Free Tier for the first 12 months (750 h/month). If you have already used yours up, it costs in the order of 0.01 USD/hour ineu-west-1. The final section explains how to delete everything.
Creating the instance from the CLI with the tagging scheme
The same result, in a single reproducible command. First the minimal security group (it is detailed in 03-02; here it is purely instrumental):
# Create the security group in the default VPC
SG_ID=$(aws ec2 create-security-group \
--group-name sg-mercadofresco-tienda \
--description "Web and SSH access for the MercadoFresco storefront" \
--query 'GroupId' --output text \
--profile mercadofresco-dev --region eu-west-1)
# Open HTTP to the whole world (it is a public shop)
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" --protocol tcp --port 80 --cidr 0.0.0.0/0 \
--profile mercadofresco-dev --region eu-west-1
# Open SSH ONLY to your current IP
MY_IP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress \
--group-id "$SG_ID" --protocol tcp --port 22 --cidr "${MY_IP}/32" \
--profile mercadofresco-dev --region eu-west-1And now the instance:
aws ec2 run-instances \
--image-id "$AMI_ID" \
--instance-type t3.micro \
--key-name mercadofresco-tienda \
--security-group-ids "$SG_ID" \
--user-data file://user-data-tienda.sh \
--metadata-options "HttpTokens=required,HttpPutResponseHopLimit=1" \
--credit-specification "CpuCredits=standard" \
--tag-specifications \
'ResourceType=instance,Tags=[
{Key=Name,Value=mercadofresco-tienda-01},
{Key=Proyecto,Value=mercadofresco},
{Key=Entorno,Value=desarrollo},
{Key=Componente,Value=tienda},
{Key=Propietario,Value=luis},
{Key=CentroCoste,Value=operaciones}]' \
'ResourceType=volume,Tags=[
{Key=Proyecto,Value=mercadofresco},
{Key=Entorno,Value=desarrollo},
{Key=Componente,Value=tienda},
{Key=Propietario,Value=luis},
{Key=CentroCoste,Value=operaciones}]' \
--profile mercadofresco-dev --region eu-west-1Parameter by parameter, because each one has its reason:
--user-data file://…: thefile://prefix is mandatory; without it, the CLI would send the literal stringuser-data-tienda.shas the script. The CLI v2 base64-encodes the file for you.--metadata-options HttpTokens=required: enforces IMDSv2.HttpPutResponseHopLimit=1stops a container inside the instance from reaching the host's metadata.--credit-specification CpuCredits=standard: while testing we would rather the instance throttled than generated unexpected charges fromunlimited.--tag-specifications: it is passed twice, once for the instance and once for the volume. This is the most frequent tagging mistake: the instance gets tagged, the disk is forgotten, and in module 11 an EBS charge turns up that nobody can attribute to anyone.
Check the result with a readable query:
aws ec2 describe-instances \
--filters "Name=tag:Proyecto,Values=mercadofresco" \
"Name=instance-state-name,Values=running" \
--query 'Reservations[].Instances[].{
ID:InstanceId,
Type:InstanceType,
State:State.Name,
IP:PublicIpAddress,
AZ:Placement.AvailabilityZone,
Name:Tags[?Key==`Name`]|[0].Value}' \
--output table \
--profile mercadofresco-dev --region eu-west-1Launch templates and Auto Scaling: the answer to the Friday peak
We now have a server. But one server does not solve problem 1: on Fridays from 17:00 to 21:00 900 orders/hour arrive and one instance handles 600. And buying a bigger machine "for Friday" means paying for it all 168 hours of the week in order to use it for 4.
AWS's answer is two pieces that work together.
Launch template
This is the versioned recipe for what a storefront instance must look like: AMI, type, key pair,
security group, user data, tags. There is no longer "the instance Luis built by hand"; there is a
definition anyone can reproduce identically.
# The user data must go base64-encoded inside the template's JSON
USER_DATA_B64=$(base64 -w0 user-data-tienda.sh)
aws ec2 create-launch-template \
--launch-template-name lt-mercadofresco-tienda \
--version-description "v1 nginx + php-fpm" \
--launch-template-data "{
\"ImageId\": \"$AMI_ID\",
\"InstanceType\": \"t3.micro\",
\"KeyName\": \"mercadofresco-tienda\",
\"SecurityGroupIds\": [\"$SG_ID\"],
\"UserData\": \"$USER_DATA_B64\",
\"MetadataOptions\": {\"HttpTokens\": \"required\"},
\"TagSpecifications\": [{
\"ResourceType\": \"instance\",
\"Tags\": [
{\"Key\": \"Name\", \"Value\": \"mercadofresco-tienda-asg\"},
{\"Key\": \"Proyecto\", \"Value\": \"mercadofresco\"},
{\"Key\": \"Entorno\", \"Value\": \"desarrollo\"},
{\"Key\": \"Componente\", \"Value\": \"tienda\"},
{\"Key\": \"Propietario\", \"Value\": \"luis\"},
{\"Key\": \"CentroCoste\", \"Value\": \"operaciones\"}
]}]
}" \
--profile mercadofresco-dev --region eu-west-1Templates are versioned: when you change the user data you create version 2 and you can go back
to version 1 if something goes wrong. That ability to roll back is the first step towards problem
4 (risky deployments), which will be solved in full in module 8.
Auto Scaling group (ASG)
An ASG keeps a number of healthy instances spread across several Availability Zones, and adjusts it according to demand. It has three numbers:
| Parameter | Meaning | Value at MercadoFresco |
|---|---|---|
| Minimum | It will never go below this | 2 (one per AZ: fault tolerance) |
| Desired | How many it wants to have right now | 2 at rest |
| Maximum | It will never go above this (spending cap) | 4 |
The sizing comes out of the real figures:
Friday peak: 900 orders/hour Capacity per instance: 600 orders/hour Instances needed: 900 / 600 = 1.5 → 2 as a working minimum Margin for one AZ failing: +1 Maximum with room to grow: 4
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name asg-mercadofresco-tienda \
--launch-template "LaunchTemplateName=lt-mercadofresco-tienda,Version=\$Latest" \
--min-size 2 --max-size 4 --desired-capacity 2 \
--vpc-zone-identifier "subnet-aaa11111,subnet-bbb22222" \
--health-check-type EC2 --health-check-grace-period 120 \
--tags "Key=Proyecto,Value=mercadofresco,PropagateAtLaunch=true" \
"Key=Entorno,Value=desarrollo,PropagateAtLaunch=true" \
--profile mercadofresco-dev --region eu-west-1--vpc-zone-identifier: two subnets in two different AZs (eu-west-1aandeu-west-1b), just as we decided in 01-03. Replace the IDs with those of your default VPC.--health-check-grace-period 120: gives theuser data2 minutes before judging whether the instance is healthy. Without this wait, the ASG kills instances that were still installing.PropagateAtLaunch=true: the tags are copied onto every new instance. Without this, the machines created by the ASG would show up untagged on the bill.
And the policy that does the work automatically:
# Target tracking scaling: "keep the group's average CPU at 60 %".
aws autoscaling put-scaling-policy \
--auto-scaling-group-name asg-mercadofresco-tienda \
--policy-name cpu-objetivo-60 \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"TargetValue": 60.0,
"PredefinedMetricSpecification": {"PredefinedMetricType": "ASGAverageCPUUtilization"}
}' \
--profile mercadofresco-dev --region eu-west-1Target tracking is the recommended policy: you declare the goal and AWS works out on its own when to add and when to remove machines. Alternatives: step scaling (manual thresholds) and scheduled scaling, which makes a lot of sense for MercadoFresco because the peak happens at a known time:
# Go up to 4 instances every Friday at 16:45 UTC, before the peak arrives.
aws autoscaling put-scheduled-update-group-action \
--auto-scaling-group-name asg-mercadofresco-tienda \
--scheduled-action-name pico-viernes-tarde \
--recurrence "45 16 * * 5" \
--desired-capacity 4 \
--profile mercadofresco-dev --region eu-west-1The complete flow:
flowchart TD
A["Friday 17:00<br/>900 orders/hour arrive"] --> B["CloudWatch measures<br/>the group's average CPU"]
B --> C{"Average CPU<br/>> 60 %?"}
C -->|Yes| D["ASG launches instances<br/>from lt-mercadofresco-tienda"]
D --> E["user data installs<br/>nginx + php-fpm"]
E --> F["Health check<br/>/salud returns 200"]
F --> G["Instance in service<br/>capacity = 4 x 600 = 2,400 orders/h"]
C -->|No, Saturday 03:00| H["ASG scales down to the minimum<br/>2 instances"]
H --> I["You stop paying for<br/>what you are not using"]
One piece is still missing for this to work for real: something that spreads the traffic across the instances. That is Elastic Load Balancing, lesson 03-03. Here we have built the engine; in module 3 we will connect the steering to it.
Shutting everything down and deleting it so you do not spend
Golden rule of this course: whatever you create in a lesson, you delete when you finish it.
# 1. Empty and delete the Auto Scaling group (--force-delete terminates its instances)
aws autoscaling delete-auto-scaling-group \
--auto-scaling-group-name asg-mercadofresco-tienda --force-delete \
--profile mercadofresco-dev --region eu-west-1
# 2. Delete the launch template
aws ec2 delete-launch-template \
--launch-template-name lt-mercadofresco-tienda \
--profile mercadofresco-dev --region eu-west-1
# 3. Terminate the instance created by hand
aws ec2 terminate-instances --instance-ids i-0123456789abcdef0 \
--profile mercadofresco-dev --region eu-west-1
# 4. Verify that NOTHING carrying the project tag is left running
aws ec2 describe-instances \
--filters "Name=tag:Proyecto,Values=mercadofresco" \
"Name=instance-state-name,Values=running,pending,stopped" \
--query 'Reservations[].Instances[].[InstanceId,State.Name]' --output table \
--profile mercadofresco-dev --region eu-west-1If you want to keep the instance for the next lesson without paying for compute, stop it instead of terminating it (you will still pay for the 8 GiB volume only, around 0.64 USD a month):
aws ec2 stop-instances --instance-ids i-0123456789abcdef0 \
--profile mercadofresco-dev --region eu-west-1And remember to check the presupuesto-mensual-mercadofresco budget you set up in 01-02: it is your
safety net if something is left switched on.
Common Mistakes and Tips
- Terminating when you meant to stop. The irreversible mistake par excellence. Always enable
--disable-api-terminationon any instance that holds data. - Losing the
.pemfile. AWS has no copy. If you lose it, the only way out is to detach the volume and mount it on another instance. Keep it in your password manager and have Session Manager enabled as a plan B. - Opening port 22 to
0.0.0.0/0. You will get automated access attempts within minutes. Always<your-ip>/32, or better still, no SSH port at all and Session Manager. - Being surprised that CPU stays flat at 10 %. Those are exhausted burstable credits. Look at
the
CPUCreditBalancemetric before blaming the application. - Forgetting
file://in--user-data. The instance starts up "fine" but installs nothing. If the web server does not answer, log in and read/var/log/cloud-init-output.log. - Tagging the instance and not the volume. Pass
--tag-specificationsforinstanceand forvolume, always. - Expecting
user datato run on every start-up. It only runs the first time. For per-boot tasks, use a systemd unit created by theuser dataitself. - Mistaking the public IP for something stable. It changes when you stop and start. Use DNS (03-05).
- Leaving the ASG with a high
min-size"just in case". It is the quietest way to multiply the bill: every instance in the minimum is paid for 24×7. - Golden tip: start small and measure. It is infinitely cheaper to size an undersized instance up than to discover in module 11 that you have been paying double for six months.
Exercises
Exercise 1: sizing the group for growth to three cities
MercadoFresco wants to open in two more cities (problem 3). The forecast is that the Friday peak will go from 900 to 2,100 orders/hour. Each instance still handles 600 orders/hour, and Marta's policy requires that the service keeps working even if an entire Availability Zone goes down.
Work out the ASG's minimum, desired and maximum, justifying each number, and write the corresponding
aws autoscaling update-auto-scaling-group command.
Exercise 2: diagnosing a user data that does not work
Luis launches an instance with the lesson's script, the instance shows as running with 2/2 checks
green, but when he opens the public IP the browser just waits indefinitely (it does not say
"connection refused", it simply does not answer).
List, in increasing order of investigation cost, the steps you would take and what you would check at each one. Say which is the most likely cause given the "it just waits" clue.
Exercise 3: choosing type and purchase model
For each of these three MercadoFresco workloads, choose a family, an approximate size and a purchase model, and justify it in one sentence:
- A) Storefront server in production, sustained traffic 24 hours a day, 4 GiB of RAM is enough, CPU at 55 % on average.
- B) Overnight process that recompresses the catalogue's 40,000 photos; it takes 3 hours, it saturates the CPU, and if it is interrupted it can resume from where it was.
- C) Luis's development environment, switched on from 9:00 to 18:00 Monday to Friday, almost always idle with bursts when compiling.
Solutions
Solution 1.
Capacity needed at the peak: 2,100 / 600 = 3.5 → 4 instances Tolerance to one AZ failing: the group must still deliver 4 instances with one AZ fewer. With 2 AZs, half the capacity sits in each one, so it must be able to reach 8 to survive losing half of them.
- Minimum = 2. In the trough no more is needed, but never fewer than 2 so that there is one instance in each AZ and no dependency on a single one.
- Desired = 2. It is the starting point; the target tracking policy will raise it on its own. It can be combined with a scheduled action that takes it to 4 on Fridays at 16:45.
- Maximum = 8. It covers the 4 needed plus the margin so that, if an AZ goes down, the remaining 4 can be launched in the one still standing. The maximum also acts as a spending cap: if an attack or a bug drives CPU up, it will never go beyond 8 instances.
aws autoscaling update-auto-scaling-group \
--auto-scaling-group-name asg-mercadofresco-tienda \
--min-size 2 --max-size 8 --desired-capacity 2 \
--profile mercadofresco-dev --region eu-west-1Solution 2.
The key nuance: "it just waits" ≠ "connection refused". If nginx were down but the network reached the machine, the operating system would return an immediate refusal. A timeout indicates that the packets are not reaching the instance, that is, a network problem, not a software one.
Steps in increasing order of cost:
- Security group (30 seconds, without logging into the machine): check that an inbound TCP 80
rule from
0.0.0.0/0exists. This is the most likely cause.aws ec2 describe-security-groups --group-ids "$SG_ID" \\ --query 'SecurityGroups[].IpPermissions' \\ --profile mercadofresco-dev --region eu-west-1 - Public IP: verify that the instance has one (
PublicIpAddressnot null) and that you are using that one and not the private172.31.x.x. - Public subnet: the subnet must have a route to an internet gateway (this is fully understood in 03-01).
- Log in through Session Manager (needs no port 22, useful precisely when the network fails):
sudo systemctl status nginx sudo tail -50 /var/log/cloud-init-output.log curl -s localhost # if this answers, the server is fine and the problem is the network
Solution 3.
| Workload | Family and size | Purchase model | Rationale |
|---|---|---|---|
| A) Storefront in production | m6i.large (2 vCPU, 8 GiB) or m7g.large if the software is ARM-compatible |
On demand for now; a 1-year Savings Plan once there are 3 months of data | Sustained 24×7 load: a T in unlimited would end up costing more; the M gives predictable performance without credits |
| B) Overnight recompression | c6i.xlarge or bigger (CPU intensive) |
Spot | It tolerates interruptions and is resumable: it meets the spot condition exactly and saves up to 90 % |
| C) Luis's development | t3.medium |
On demand + a scheduled stop outside working hours | Idle profile with bursts: a textbook case for burstable. Switching it off at night and at weekends cuts the cost by ~75 % |
Conclusion
MercadoFresco now has its first server on AWS and, above all, it has the vocabulary and the mechanisms
to reason about it. You know that an AMI is the template and the instance the living copy, and
that an AMI's ID changes with the region, which is why you look it up instead of copying it. You know
how to decode t3.micro and to choose between the T, M, C and R families depending on whether the
load is irregular, balanced, CPU intensive or memory intensive. You have seen the CPU credit
mechanism from the inside and worked out that a t3.micro does not survive the four hours of the
Friday peak: a concrete figure, not a hunch.
You know the four purchase models and which case each one fits, and you have mastered the
lifecycle with the distinction that costs the most: stopping keeps the disk, terminating
destroys it forever. You know how to reach the machine with a key pair and also without one, through
EC2 Instance Connect or Session Manager, which is the option MercadoFresco will use in
production because it needs no open port. You have automated the storefront's complete installation
with user data, you know where to read its log when it fails, and you have protected the
metadata by requiring IMDSv2. You have created the instance in the console and from the CLI,
applying the project's tagging scheme to both the instance and its volume.
And, above all, you have put the first real piece in place against problem 1: a versioned launch template and an Auto Scaling group of 2 to 4 instances, with target tracking and scheduled scaling, sized with MercadoFresco's real figures (900 orders/h against 600 per instance). What is still missing is the load balancer that spreads the traffic across them, and that is lesson 03-03.
Before that there is a question Auto Scaling leaves exposed: if instances are born and die on their
own, where do the data live? The product photos that today sit in /var/www/fotos cannot live
inside a disk that is destroyed along with the machine. In lesson 02-02, "Block and file storage:
EBS and EFS", we will look at volumes that outlive the instance, how they are extended while
running, how they are shared between several machines, and how snapshots automated with Data
Lifecycle Manager finally close MercadoFresco's problem 2: the backups that were never
reliable.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
