In the previous lesson we toured the console and created our first bucket with the mouse. It worked, but we left one question open: how do you repeat that exact thing a hundred times, or how do you prove six months from now what you actually did? The answer is to stop clicking and start writing commands and code.

This lesson is the gateway to all serious work on AWS. The AWS CLI is the tool you will use every day to query, create and diagnose; the SDKs are the libraries your application uses to talk to AWS from the inside. Both call the same APIs as the console, so everything you learn here applies to any service in the rest of the course.

Contents

  1. Why automate: the problem with the console
  2. Installing AWS CLI v2 on Linux, macOS and Windows
  3. Anatomy of a command
  4. Configuring credentials: aws configure, files and profiles
  5. Credential precedence
  6. Access key security
  7. Output formats and filtering with --query
  8. Pagination, --dry-run and other useful options
  9. Essential exploration commands
  10. SDKs: what they are and which languages have one
  11. boto3: the Python SDK step by step
  12. An equivalent look in JavaScript
  13. CloudShell as a no-install alternative

Why automate: the problem with the console

Marta created the test bucket in the console. Now Luis needs to create three identical buckets (one per environment) with the same tags and the same configuration. With the console he would have to repeat about fifteen clicks three times over, and any slip would produce a silent difference between environments.

The four structural problems with the console:

Problem Real consequence at MercadoFresco
It is not reproducible The development environment never ends up identical to production, and bugs "only happen in production"
It is not documentable Nobody can review in Git what was done with the mouse, or find out why
It is not composable You cannot chain "list every stopped instance and tag them"
It does not scale Tagging 200 resources by hand is unworkable

The CLI solves all four: a command is text, and text can be saved, versioned, reviewed, repeated and chained. And it is the natural stepping stone towards the infrastructure as code of module 9, where we will not even write commands any more, but declarations of the desired state.

Installing AWS CLI v2 on Linux, macOS and Windows

Always use version 2. v1 is in maintenance mode, it used to be installed with pip (which caused dependency conflicts) and it lacks important features such as signing in with IAM Identity Center.

Linux (x86_64)

# 1. Download the official installer as a zip file
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"

# 2. Unpack the archive into the current directory
unzip awscliv2.zip

# 3. Run the installer with administrator privileges
sudo ./aws/install

# 4. Clean up the temporary files
rm -rf awscliv2.zip aws/

What each line does:

  • curl ... -o "awscliv2.zip": downloads the package and saves it under that name. The URL is the official AWS one; be wary of third-party installers.
  • unzip: extracts an aws/ folder containing the installer.
  • sudo ./aws/install: copies the binaries to /usr/local/aws-cli and creates the /usr/local/bin/aws link. It needs sudo because it writes outside your home directory.
  • rm -rf: deletes what you downloaded, which is no longer needed.

If your machine is ARM (a Graviton instance, for example), swap x86_64 for aarch64.

To upgrade an existing installation, add the update parameters:

sudo ./aws/install --bin-dir /usr/local/bin --install-dir /usr/local/aws-cli --update

macOS

# Download the official .pkg package
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"

# Install for every user on the machine
sudo installer -pkg AWSCLIV2.pkg -target /

An alternative with Homebrew, if you already use it: brew install awscli.

Windows

Download and run the official MSI installer:

https://awscli.amazonaws.com/AWSCLIV2.msi

Or from PowerShell as administrator:

msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi

Afterwards close and reopen the terminal so that the PATH variable is reloaded.

Verification

On any system:

aws --version

Expected output (the numbers will vary):

aws-cli/2.15.40 Python/3.11.8 Linux/6.5.0 exe/x86_64.ubuntu.22

Read it like this: CLI version, embedded Python version (it does not use your Python, it ships with its own), operating system and build type. If you see aws-cli/1.x, you have v1 and you should uninstall it before carrying on.

Anatomy of a command

Every command follows the same structure:

aws <service> <operation> [--parameter value] [--global-options]

A real example, line by line:

aws ec2 describe-instances \
  --filters "Name=instance-state-name,Values=running" \
  --region eu-west-1 \
  --output table
Part What it is Detail
aws The program
ec2 Service Usually matches the service name: s3, iam, lambda, rds
describe-instances Operation The API is called DescribeInstances; the CLI uses lower case and hyphens
--filters "..." Operation-specific parameter Filters on the server: running instances only
--region Global option Overrides the profile's region for this command
--output table Global option Output format

The verbs follow very predictable conventions, and recognising them speeds up learning enormously:

Prefix What it does Examples
describe- Returns detailed information describe-instances, describe-vpcs
list- Returns a list of identifiers list-buckets, list-functions
get- Fetches one specific item get-caller-identity, get-bucket-tagging
create- Creates a resource create-bucket, create-tags
delete- / terminate- Removes something delete-bucket, terminate-instances
put- Writes or overwrites a configuration put-bucket-tagging, put-metric-alarm

The built-in help

It is the best documentation you have and it works offline:

aws help                        # lists every available service
aws s3api help                  # lists every s3api operation
aws s3api create-bucket help    # every parameter of that operation, with examples

It opens in a pager: navigate with the arrow keys and quit with q.

Configuring credentials: aws configure, files and profiles

The CLI needs to know who you are. We are going to create access keys for the mercadofresco-admin user we created in lesson 01-02.

Step 1: create the access keys

In the console: IAM → Users → mercadofresco-admin → Security credentials → Create access key. Choose the Command Line Interface (CLI) use case, confirm the warning and create.

You will get two values:

  • Access key ID: something like AKIAIOSFODNN7EXAMPLE. It is public-ish; it identifies the key.
  • Secret access key: something like wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY. It is shown only once. If you lose it, you have to create a new key.

Warning: never create access keys for the root user (lesson 01-02). And if you would rather not handle permanent keys at all, the modern alternative is aws configure sso with IAM Identity Center, which issues temporary credentials.

Step 2: aws configure

aws configure

It will ask you four questions:

AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: eu-west-1
Default output format [None]: json
  • Default region: eu-west-1, the one we settled on in lesson 01-03. It saves you writing --region on every command.
  • Output format: json is a good default value because it is the one consumed by other tools.

The configuration files

aws configure performs no magic: it writes two text files in your home directory (~/.aws/ on Linux and macOS, %USERPROFILE%\.aws\ on Windows).

~/.aws/credentials — holds the secrets:

[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

~/.aws/config — holds the non-secret settings:

[default]
region = eu-west-1
output = json

You can edit them by hand perfectly well. Check as well that the permissions are restrictive:

chmod 600 ~/.aws/credentials

This leaves the file readable and writable by your user only. On a shared machine it is essential.

Named profiles

You will almost never work with a single identity. MercadoFresco will soon have a development environment and a production one, and it is important that the two cannot be confused.

Create a new profile:

aws configure --profile mercadofresco-dev

The files then look like this:

# ~/.aws/credentials
[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

[mercadofresco-dev]
aws_access_key_id = AKIAI44QH8DHBEXAMPLE
aws_secret_access_key = je7MtGbClwBF/2Zp9Utk/h3yCoEXAMPLEKEY
# ~/.aws/config
[default]
region = eu-west-1
output = json

[profile mercadofresco-dev]
region = eu-west-1
output = json

Mind the detail: in credentials the section is [mercadofresco-dev], but in config it is [profile mercadofresco-dev], with the word profile in front. It is a historical inconsistency that causes constant errors. If a profile "cannot be found", check this first.

To use a profile:

# Option 1: as a parameter, on one specific command
aws s3 ls --profile mercadofresco-dev

# Option 2: as an environment variable, for the whole terminal session
export AWS_PROFILE=mercadofresco-dev
aws s3 ls

And to always know who you are acting as:

aws sts get-caller-identity --profile mercadofresco-dev

Get into the habit of running this command before any destructive operation. It is the difference between deleting the development database and deleting the production one.

Credential precedence

When you run a command, the CLI looks for credentials in a fixed order and stops at the first source it finds. Knowing this order explains 90 % of all "but I did configure that" moments:

# Source How it is given When it is used
1 Command line options --profile, --region Overrides everything else
2 Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_PROFILE, AWS_DEFAULT_REGION CI/CD, containers, temporary sessions
3 The ~/.aws/credentials file The [default] profile or the one you name Day-to-day work on your laptop
4 The ~/.aws/config file The [profile ...] section Configuration and roles
5 Container credentials A variable injected by ECS/EKS Containers inside AWS (module 10)
6 Instance role (IMDS) The EC2 instance's own metadata The correct way inside AWS

The last two are the conceptually important ones. When your code runs inside AWS —on an EC2 instance, in a Lambda function, in a container— it needs no keys at all: it is assigned an IAM role and the SDK automatically obtains temporary credentials that rotate themselves every few hours.

flowchart TB
    A["Command or SDK needs credentials"] --> B{"--profile on the line?"}
    B -->|yes| USA1["Use that profile"]
    B -->|no| C{"Environment variables?"}
    C -->|yes| USA2["Use the variables"]
    C -->|no| D{"~/.aws/credentials file?"}
    D -->|yes| USA3["Use the default profile"]
    D -->|no| E{"Running inside AWS?"}
    E -->|yes| USA4["Instance role - temporary credentials - THE IDEAL"]
    E -->|no| F["Error: Unable to locate credentials"]

IAM roles are studied in depth in lesson 04-01. Hold on to the idea: permanent keys on your laptop only; inside AWS, always roles.

Access key security

An access key for mercadofresco-admin is, quite literally, full control of the account. These rules are not optional.

Never in the code and never in Git

This is the mistake that ruins accounts every single day. There are bots continuously scanning GitHub for strings starting with AKIA; a published key is exploited within minutes, typically by launching dozens of large instances to mine cryptocurrency. The resulting bill can run into thousands of euros.

Minimum protection in any repository:

# .gitignore
.env
.aws/
*.pem
credentials

And an automatic barrier, which is highly recommended:

# git-secrets scans every commit for AWS credential patterns
git secrets --install
git secrets --register-aws

If you ever publish a key by mistake: deactivate and delete it in IAM immediately, check CloudTrail (lesson 05-03) to see what was done with it, and create a new one. Deleting the commit achieves nothing: it has already been indexed.

Other rules

Rule Why
Rotate every 90 days Shrinks the exploitation window for a leaked key
One key per person and use Lets you revoke one without affecting the rest
Delete inactive keys IAM shows the last-used date; whatever is not used gets deleted
Never keys on root Already covered in 01-02
Inside AWS, roles instead of keys Temporary credentials, rotated automatically
MFA for sensitive operations It can be required in IAM policies (lesson 04-01)

The procedure for rotating without downtime is simple: create a second key, update wherever it is used, check that everything works, deactivate the old one, wait a few days and delete it. IAM allows two active keys per user precisely for this.

Output formats and filtering with --query

AWS commands return a great deal of information. Learning to trim it is what turns the CLI into a comfortable tool.

Output formats

Format When to use it
json The default. Ideal for piping into jq or processing in scripts
table For reading with your eyes: ASCII borders and columns
text For processing with grep, awk or cut; tab separated
yaml More readable than JSON for long configurations
aws ec2 describe-regions --output table

--query with JMESPath

--query applies a JMESPath expression to the response, on the client. Let us build up step by step.

Level 1 — extract a list:

# Returns only the complete Regions array
aws ec2 describe-regions --query 'Regions'

Level 2 — one field from each element:

# From each region, just its name. The [] brackets walk the list
aws ec2 describe-regions --query 'Regions[].RegionName' --output text

Output:

eu-west-1	eu-west-2	eu-west-3	us-east-1	...

Level 3 — rename fields into an object:

aws ec2 describe-regions \
  --query 'Regions[].{Region:RegionName, Endpoint:Endpoint}' \
  --output table

The braces {...} build a new object; to the left of the : goes the name you want to see, to the right the original field.

Level 4 — filter with a condition:

# European regions only: ? introduces the filter, contains() is a JMESPath function
aws ec2 describe-regions \
  --query 'Regions[?contains(RegionName, `eu-`)].RegionName' \
  --output table

Note the backticks around eu-: in JMESPath, literals go inside backticks, not inside ordinary quotes. It is a frequent source of errors.

Level 5 — a real MercadoFresco case:

aws ec2 describe-instances \
  --query 'Reservations[].Instances[].{
      Id:InstanceId,
      Type:InstanceType,
      State:State.Name,
      IP:PrivateIpAddress,
      Zone:Placement.AvailabilityZone,
      Name:Tags[?Key==`Name`]|[0].Value
    }' \
  --output table

Breakdown:

  • Reservations[].Instances[]: flattens EC2's nested structure (instances come grouped by reservation) into a single list.
  • State.Name: navigates inside a nested object with the dot.
  • Tags[?Key==\Name`]|[0].Value: filters the tags down to the one with key Name, and the |[0]` takes the first result of that list in order to extract its value.

--query versus --filters

--filters --query
Where it is applied On the AWS server On your machine, after receiving the response
Effect Reduces what is transferred Only reduces what is displayed
Availability Only on some services and fields On any command
Recommendation Use it whenever it exists To shape the result

The ideal is to combine them: --filters to bring back little, --query to present it well.

Pagination, --dry-run and other useful options

Pagination

When there are many results, the CLI v2 paginates them automatically and opens a pager. Options to control it:

# Returns at most 5 items
aws s3api list-buckets --max-items 5

# Disables the interactive pager (useful in scripts)
aws ec2 describe-instances --no-cli-pager

# Disable it permanently
export AWS_PAGER=""

If you limit with --max-items and there are more results, the response includes a NextToken that you can pass with --starting-token to request the next page.

--dry-run

Many EC2 operations accept --dry-run: they check whether you would have permission to perform the operation, but they do not run it.

aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.micro \
  --dry-run

If you have permission, you will see:

An error occurred (DryRunOperation) when calling the RunInstances operation:
Request would have succeeded, but DryRun flag is set

That message, even though it says "error", is the confirmation of success. If you did not have permission, the error would be UnauthorizedOperation. It is a safe way of verifying permissions without creating anything and without spending anything.

Other options that get a lot of use

Option What for
--no-cli-pager Direct output with no pager
--debug Full trace of the HTTP request; indispensable for diagnosing
--cli-input-json file://params.json Pass every parameter from a file
--generate-cli-skeleton Generate the JSON parameter template for an operation
--endpoint-url Point at an alternative endpoint (a local emulator, for instance)

Essential exploration commands

These are the commands you will run hundreds of times. Keep them somewhere safe.

# 1. Who am I? Returns account, user ID and ARN
aws sts get-caller-identity
# 2. Which regions exist and are enabled in my account?
aws ec2 describe-regions --query 'Regions[].RegionName' --output text
# 3. Which Availability Zones does my region have, with their physical ID?
aws ec2 describe-availability-zones \
  --query 'AvailabilityZones[].{AZ:ZoneName, Id:ZoneId, State:State}' \
  --output table
# 4. Which buckets do I have?
aws s3 ls
# 5. Which EC2 instances do I have, and in what state?
aws ec2 describe-instances \
  --query 'Reservations[].Instances[].{Id:InstanceId, State:State.Name}' \
  --output table
# 6. What configuration does the CLI have, and where does each value come from?
aws configure list

The output of the last one is especially useful because it indicates the origin of each value:

      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile     mercadofresco-dev           env    AWS_PROFILE
access_key     ****************MPLE   shared-credentials-file
    region                eu-west-1      config-file    ~/.aws/config

When something does not work the way you expect, this command tells you exactly which credential and which region the CLI is using, and why.

SDKs: what they are and which languages have one

An SDK (Software Development Kit) is a library that lets your application call the AWS APIs from the language it is written in, without building signed HTTP requests by hand.

The difference from the CLI:

CLI SDK
Who uses it A person at a terminal, or a script Your application, at runtime
Typical case "List the buckets", "stop that instance" "When the customer uploads a photo, store it in S3"
Format Text commands Code in your language

Languages with an official SDK:

Language SDK name Note
Python boto3 The most widely used for scripting and automation
JavaScript / TypeScript AWS SDK for JavaScript v3 Modular: you install only the client you use
Java AWS SDK for Java 2.x Very common in the enterprise
.NET (C#) AWS SDK for .NET
Go AWS SDK for Go v2 Common in infrastructure tooling
PHP AWS SDK for PHP Relevant to MercadoFresco's current monolith
Rust, Ruby, C++, Kotlin, Swift Available

They all share the same logic: a credential provider chain (the same order of precedence we saw), automatic retries with exponential backoff, and operation names equivalent to those of the API.

boto3: the Python SDK step by step

Luis is going to use Python for MercadoFresco's scripts, so let us look at boto3 in detail.

Installation

# Create an isolated virtual environment (good practice: do not install into the system Python)
python3 -m venv venv

# Activate it (on Windows: venv\Scripts\activate)
source venv/bin/activate

# Install boto3
pip install boto3

Client versus resource

boto3 offers two different interfaces to the same thing:

Client Resource
Level Low: mirrors the API 1:1 High: object-oriented
Coverage Every service and operation Only some services; in maintenance
Style s3.list_buckets() returns a dictionary for b in s3.buckets.all()
Recommendation Use it by default Only if you run into it in legacy code

The same goal with both:

import boto3

# --- Client interface (recommended) ---
s3_client = boto3.client("s3")
response = s3_client.list_buckets()           # returns a dictionary exactly as the API gives it
for bucket in response["Buckets"]:
    print(bucket["Name"], bucket["CreationDate"])

# --- Resource interface (older) ---
s3_resource = boto3.resource("s3")
for bucket in s3_resource.buckets.all():      # objects with attributes
    print(bucket.name, bucket.creation_date)

Credentials and sessions

boto3 uses exactly the same precedence order as the CLI, so if aws s3 ls works, your Python code will work with nothing to configure.

To choose a profile or a region explicitly you use a session:

import boto3

# A session wraps credentials + region
session = boto3.Session(profile_name="mercadofresco-dev", region_name="eu-west-1")
s3 = session.client("s3")

print(session.client("sts").get_caller_identity()["Arn"])

Error handling with ClientError

Every error returned by an AWS service arrives as botocore.exceptions.ClientError. Ignoring it is the most common mistake in a beginner's scripts.

import boto3
from botocore.exceptions import ClientError

s3 = boto3.client("s3", region_name="eu-west-1")

def bucket_exists(name: str) -> bool:
    """Checks whether a bucket exists and is reachable with our credentials."""
    try:
        s3.head_bucket(Bucket=name)            # lightweight request: headers only
        return True
    except ClientError as error:
        # The specific code is always in error.response["Error"]["Code"]
        code = error.response["Error"]["Code"]
        if code == "404":
            print(f"Bucket '{name}' does not exist.")
        elif code == "403":
            print(f"Bucket '{name}' exists but you have no permission to see it.")
        else:
            raise                              # any other error, let it propagate
        return False

print(bucket_exists("mercadofresco-fotos-producto"))

Key points in the snippet:

  • head_bucket is the cheap way to check existence: it downloads no content.
  • error.response["Error"]["Code"] is the standard path for reading the error code. Memorise it.
  • Telling 404 apart from 403 matters: "it does not exist" and "it exists but you cannot see it" demand different actions.
  • The final raise avoids the antipattern of swallowing unknown errors, which hides real problems.

Pagination in boto3

APIs return at most a few hundred items per call. If you iterate only the first response, you will lose data silently. Paginators solve it:

import boto3

s3 = boto3.client("s3", region_name="eu-west-1")

paginator = s3.get_paginator("list_objects_v2")
pages = paginator.paginate(Bucket="mercadofresco-fotos-producto", Prefix="2026/")

total = 0
for page in pages:                         # each iteration is one API call
    for obj in page.get("Contents", []):        # .get() avoids KeyError if the page comes back empty
        total += 1
print(f"Objects found: {total}")

An equivalent look in JavaScript

So that you can see the concepts carry over, here is the same bucket listing with the AWS SDK for JavaScript v3:

# The v3 SDK is modular: you install only the client for the service you need
npm install @aws-sdk/client-s3
// Import only the S3 client and the specific command we are going to use
import { S3Client, ListBucketsCommand } from "@aws-sdk/client-s3";

// The client reads credentials with the same precedence order as the CLI
const client = new S3Client({ region: "eu-west-1" });

async function listBuckets() {
  try {
    // In v3, each operation is a Command object that is sent with send()
    const response = await client.send(new ListBucketsCommand({}));
    response.Buckets.forEach((b) => console.log(b.Name, b.CreationDate));
  } catch (error) {
    // The equivalent of ClientError: the code is in error.name
    console.error("AWS error:", error.name, "-", error.message);
  }
}

listBuckets();

Differences from boto3: v3 is modular (less weight in Lambda) and uses the command pattern: you create an XxxCommand object and send it with send(). The credential, retry and error logic is equivalent.

CloudShell as a no-install alternative

As we saw in lesson 01-04, AWS CloudShell gives you a browser terminal with the CLI v2, Python and boto3 already installed, and automatically authenticated as the console user.

Local CLI CloudShell
Installation Yes No
Credentials You configure keys Inherited, temporary
Performance and local files Full Limited (1 GB persistent)
Session Permanent Expires on inactivity
Ideal for Day-to-day work, scripts, CI Quick tests, emergencies, training

CloudShell is especially useful for the "I am on a computer that is not mine and I need to diagnose something right now" scenario. It does not replace the local CLI for day-to-day work, but it is an excellent safety net.

Common Mistakes and Tips

  • Unable to locate credentials. You have not configured the profile, or you have used a profile name that does not exist. Diagnose it with aws configure list.
  • ProfileNotFound with the profile spelt correctly. It is almost always the ~/.aws/config section without the profile prefix: it must be [profile mercadofresco-dev], not [mercadofresco-dev].
  • You must specify a region. Neither the profile nor the environment variable nor the command states a region. Add --region or set it in the profile.
  • An unexpected AccessDenied. Check which identity you are acting as first, with aws sts get-caller-identity; very often it is the wrong profile, not a permissions problem.
  • Pushing keys to Git. The most expensive and most frequent disaster. .gitignore, git secrets and, better still, temporary credentials.
  • Using permanent keys inside an EC2 instance. Never. You use an instance role (lesson 04-01), which rotates the credentials for you as a bonus.
  • Forgetting pagination. Your script says "there are 1,000 objects" when there are 50,000. Use paginators in boto3 and --max-items/NextToken in the CLI.
  • Confusing aws s3 with aws s3api. The first is high level (ls, cp, sync); the second exposes the full API (get-bucket-tagging, put-bucket-policy). If an option does not exist in s3, look for it in s3api.
  • Ignoring ClientError. A script that fails silently is worse than one that breaks.
  • Tip: define aliases in your terminal for whatever you repeat a lot, for example alias whoami-aws='aws sts get-caller-identity'.
  • Tip: when a command does not work and you cannot see why, add --debug. You will see the full signed request and the service's response.

Exercises

Exercise 1: installation and exploration

  1. Install AWS CLI v2 on your system and check the version.
  2. Configure a profile called mercadofresco-admin with region eu-west-1 and json output.
  3. Run aws sts get-caller-identity and note down your full ARN.
  4. Write a single command that shows, in table format, every region whose name starts with eu-, with a single column called Region.

Exercise 2: bash inventory script

Write a script inventario-mercadofresco.sh that:

  1. Checks which identity and which account it is running under, and displays it.
  2. Lists every S3 bucket in the account.
  3. Lists the Availability Zones of eu-west-1 with their physical ID.
  4. Lists the EC2 instances with their ID, type, state and zone (if there are none, it must say so without failing).
  5. All of it with table output and without opening the interactive pager.

Exercise 3: boto3 script that creates and tags a bucket

Write a Python script crear_bucket_mercadofresco.py that:

  1. Takes the bucket name as a command line argument.
  2. Checks whether it already exists (telling "does not exist" apart from "no permissions").
  3. If it does not exist, creates it in eu-west-1 with public access block enabled.
  4. Applies the tags Proyecto=mercadofresco, Entorno=pruebas, Componente=formacion.
  5. Displays the resulting tags.
  6. Handles errors with ClientError and returns a non-zero exit code if it fails.

Cost: an empty bucket generates no appreciable cost and the Free Tier covers 5 GB. Even so, delete it when you are done with aws s3 rb s3://<bucket-name>.

Solutions

Solution 1

# 1. Installation on Linux and verification
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip && sudo ./aws/install
aws --version

# 2. Profile
aws configure --profile mercadofresco-admin
# Region: eu-west-1 ; Output: json

# 3. Identity
aws sts get-caller-identity --profile mercadofresco-admin

# 4. European regions in a table, single column called Region
aws ec2 describe-regions \
  --query 'Regions[?starts_with(RegionName, `eu-`)].{Region:RegionName}' \
  --output table

You can use either starts_with() or contains(); starts_with is more precise because contains would also accept a name carrying eu- in the middle.

Solution 2

#!/usr/bin/env bash
# inventario-mercadofresco.sh - quick inventory of the AWS account

set -euo pipefail            # abort on error, undefined variable or failure in a pipeline

PROFILE="${1:-mercadofresco-admin}"  # first argument, or the default value
REGION="eu-west-1"
export AWS_PAGER=""                  # disables the interactive pager

echo "===== IDENTITY ====="
aws sts get-caller-identity \
  --profile "$PROFILE" \
  --query '{Account:Account, Identity:Arn}' \
  --output table

echo "===== S3 BUCKETS ====="
aws s3api list-buckets \
  --profile "$PROFILE" \
  --query 'Buckets[].{Name:Name, Created:CreationDate}' \
  --output table

echo "===== AVAILABILITY ZONES IN $REGION ====="
aws ec2 describe-availability-zones \
  --profile "$PROFILE" --region "$REGION" \
  --query 'AvailabilityZones[].{AZ:ZoneName, PhysicalId:ZoneId, State:State}' \
  --output table

echo "===== EC2 INSTANCES ====="
INSTANCES=$(aws ec2 describe-instances \
  --profile "$PROFILE" --region "$REGION" \
  --query 'Reservations[].Instances[].{Id:InstanceId, Type:InstanceType, State:State.Name, Zone:Placement.AvailabilityZone}' \
  --output table)

if [ -z "$INSTANCES" ]; then
  echo "(no EC2 instances in $REGION)"
else
  echo "$INSTANCES"
fi

echo "===== END OF INVENTORY ====="

Important details:

  • set -euo pipefail stops the script from merrily carrying on after a failure.
  • export AWS_PAGER="" is essential in scripts: without it, the CLI would open a pager and the script would hang waiting for somebody to press q.
  • Storing the output in a variable lets you check whether it is empty and give a clear message instead of printing an empty table.

You run it with:

chmod +x inventario-mercadofresco.sh
./inventario-mercadofresco.sh mercadofresco-admin

Solution 3

#!/usr/bin/env python3
"""crear_bucket_mercadofresco.py - creates and tags an S3 bucket for MercadoFresco."""

import sys
import boto3
from botocore.exceptions import ClientError

REGION = "eu-west-1"
TAGS = [
    {"Key": "Proyecto",   "Value": "mercadofresco"},
    {"Key": "Entorno",    "Value": "pruebas"},
    {"Key": "Componente", "Value": "formacion"},
]


def bucket_exists(s3, name: str) -> bool:
    """True if the bucket exists and is reachable; False if it does not exist."""
    try:
        s3.head_bucket(Bucket=name)
        return True
    except ClientError as error:
        code = error.response["Error"]["Code"]
        if code in ("404", "NoSuchBucket"):
            return False
        if code == "403":
            print(f"Bucket '{name}' exists but belongs to another account.")
            sys.exit(1)
        raise


def create_bucket(s3, name: str) -> None:
    """Creates the bucket in REGION with public access blocked."""
    # Outside us-east-1 you have to state LocationConstraint explicitly
    s3.create_bucket(
        Bucket=name,
        CreateBucketConfiguration={"LocationConstraint": REGION},
    )
    print(f"Bucket '{name}' created in {REGION}.")

    # Total public access block: all four options set to True
    s3.put_public_access_block(
        Bucket=name,
        PublicAccessBlockConfiguration={
            "BlockPublicAcls": True,
            "IgnorePublicAcls": True,
            "BlockPublicPolicy": True,
            "RestrictPublicBuckets": True,
        },
    )
    print("Public access blocked.")


def apply_tags(s3, name: str) -> None:
    """Applies the tag set (overwriting any existing tags)."""
    s3.put_bucket_tagging(Bucket=name, Tagging={"TagSet": TAGS})
    print("Tags applied.")


def main() -> int:
    if len(sys.argv) != 2:
        print("Usage: python crear_bucket_mercadofresco.py <bucket-name>")
        return 1

    name = sys.argv[1]
    s3 = boto3.client("s3", region_name=REGION)

    try:
        if bucket_exists(s3, name):
            print(f"Bucket '{name}' already exists in your account; it will not be created again.")
        else:
            create_bucket(s3, name)

        apply_tags(s3, name)

        current = s3.get_bucket_tagging(Bucket=name)["TagSet"]
        print("\nCurrent tags on the bucket:")
        for tag in current:
            print(f"  {tag['Key']} = {tag['Value']}")

    except ClientError as error:
        print(f"AWS error: {error.response['Error']['Code']} - "
              f"{error.response['Error']['Message']}", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())

Keys to the solution:

  • LocationConstraint: if you create a bucket in any region other than us-east-1 and omit this parameter, S3 creates it in us-east-1. It is one of the historical oddities of the API.
  • put_public_access_block with all four options set to True is the protection that prevents the most common data leak on AWS.
  • put_bucket_tagging overwrites the complete tag set: if you wanted to add one without deleting the others, you would have to read them first with get_bucket_tagging and merge them.
  • Exit codes: 0 if all goes well, 1 if it fails, so that the script can be chained into a pipeline.

Running it and cleaning up:

python crear_bucket_mercadofresco.py mercadofresco-pruebas-cli-jc-2741

# Mandatory clean-up when you are done (rb = remove bucket; it must be empty)
aws s3 rb s3://mercadofresco-pruebas-cli-jc-2741

Conclusion

With this lesson you close the introductory module and, above all, make the leap from "AWS user" to "AWS operator". You have installed the AWS CLI v2, you have understood the anatomy of a command (aws <service> <operation> --parameters) and the naming conventions that let you guess commands you do not know yet. You have configured credentials with aws configure, you have seen what is really inside ~/.aws/credentials and ~/.aws/config, and you know how to work with named profiles so as never to confuse development with production. You know credential precedence, which explains almost every strange behaviour, and the non-negotiable security rules: never keys in Git, periodic rotation and, inside AWS, roles instead of keys.

You have learned to tame command output with --output and with --query and JMESPath, to avoid the silent trap of pagination, to verify permissions without spending anything using --dry-run, and you have in hand the handful of exploration commands you will use every day. And you have taken your first steps with the SDKs: boto3 in Python, with the distinction between client and resource, correct error handling through ClientError and paginators, plus the JavaScript equivalent to show that the concepts are universal.

MercadoFresco now has everything it needs to start in earnest: a secure, monitored account, a region chosen with judgement, a console you know how to drive, and command line and programming tools that make the work repeatable.

In module 2, "Core AWS services", starting with lesson 02-01 "Amazon EC2", we will stand up MercadoFresco's first real server in the cloud and begin the migration that will solve, one by one, the four problems we uncovered in the very first lesson.

© Copyright 2026. All rights reserved