Lab 183 · EBS + S3 · CLI & Python

Managing Storage: EBS Snapshots & S3 Versioning

Two storage flows in one lab: automated EBS snapshot retention via cron + a Python script using boto3, and a full S3 sync cycle that exercises versioning to recover a deleted file from a delete marker.

Summary

The lab combines four tasks. Tasks 1–2 set up the S3 bucket / IAM role and run the manual and scheduled EBS snapshot flow. Tasks 3–4 (the challenge) cover the S3 sync cycle with versioning, including recovering an object after a delete marker is created.

EBS Snapshot Automation

Take an initial point-in-time snapshot of the Processor's EBS volume, then schedule recurring snapshots with cron and retain only the last two using a Python script.

S3 Sync with Versioning

Enable versioning on a bucket, sync a local directory, propagate deletes with --delete, and recover the deleted object from its previous version.

What This Proves

That aws s3 sync --delete does not destroy data when versioning is on — it creates a delete marker. The previous version remains accessible by VersionId.

Task 1 — Bucket + IAM role for the Processor

Setup task: create the S3 bucket that the challenge will use and grant the Processor instance the permissions it needs to talk to EBS and S3.

1.1

Create the S3 bucket

A general-purpose S3 bucket was created in the lab's default region. The chosen name was superbucket3k, used throughout the rest of the lab as the sync target. Versioning was left disabled at this point — it gets enabled later in Task 4, just before the sync that requires recovery.

1.2

Attach the S3BucketAccess role to the Processor

The lab pre-creates an IAM role named S3BucketAccess. From the EC2 console, Actions → Security → Modify IAM role on the Processor instance attached this role as an instance profile. Without this step, the AWS CLI running on the Processor would not have credentials to call S3 or describe its own EBS volume.

Why an instance profile, not static keys: the Processor needs to call aws s3 and aws ec2 commands. An instance profile lets the CLI pick up temporary credentials from the EC2 metadata service — no keys on disk, no rotation problem.

Task 2 — Initial EBS snapshot from the CLI

Connect to the Command Host via EC2 Instance Connect, identify the Processor's EBS volume, stop the instance to guarantee a clean snapshot, take the snapshot, and start the instance again.

Discover the volume + instance IDs

Two describe-instances queries with JMESPath filters returned the VolumeId and the InstanceId of the Processor. These values are the inputs for every subsequent EBS call.

aws ec2 describe-instances \
  --filter 'Name=tag:Name,Values=Processor' \
  --query 'Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.{VolumeId:VolumeId}'

aws ec2 describe-instances \
  --filters 'Name=tag:Name,Values=Processor' \
  --query 'Reservations[0].Instances[0].InstanceId'

Actual IDs returned in this run: volume vol-052774238e8c5a84b, instance i-0b40f2b484a8f4733.

Stop · snapshot · start

Stopping the instance before create-snapshot guarantees the filesystem is flushed. The wait sub-commands are blocking and return only when the resource reaches the target state, which makes the flow scriptable.

aws ec2 stop-instances           --instance-ids i-0b40f2b484a8f4733
aws ec2 wait instance-stopped    --instance-id  i-0b40f2b484a8f4733
aws ec2 create-snapshot          --volume-id    vol-052774238e8c5a84b
aws ec2 wait snapshot-completed  --snapshot-id  snap-0c318bdefed94e5c4
aws ec2 start-instances          --instance-ids i-0b40f2b484a8f4733
aws ec2 wait instance-stopped followed by create-snapshot output showing snap-0c318bdefed94e5c4 in pending state
Output of create-snapshot immediately after the instance reaches the stopped state. The new snapshot snap-0c318bdefed94e5c4 is reported in pending; wait snapshot-completed would block on the next line until it reaches 100%.

Task 3 — Scheduled snapshots + retention script

The lab installs a one-minute cron entry to generate snapshots automatically, then uses a Python script (snapshotter_v2.py) to keep only the two most recent ones per volume.

Schedule recurring snapshots

The cron entry redirects both stdout and stderr to /tmp/cronlog so the job runs silently while still leaving a trail for debugging. After installing the job, a single describe-snapshots call against the volume confirms that new snapshots show up every minute (one completed, one pending in the capture).

echo "* * * * *  aws ec2 create-snapshot --volume-id vol-052774238e8c5a84b 2>&1 >> /tmp/cronlog" > cronjob
crontab cronjob

aws ec2 describe-snapshots --filters "Name=volume-id,Values=vol-052774238e8c5a84b"
Cron installed + first two snapshots visible. One snapshot is already completed, the next is still pending — exactly what one-minute cadence looks like.
crontab cronjob followed by describe-snapshots returning two Snapshots: one completed and one pending

Retention with snapshotter_v2.py

Once enough snapshots accumulate, the cron job is removed with crontab -r and snapshotter_v2.py is run. The script uses boto3 to enumerate all volumes in the account, sort the existing snapshots by start_time, and delete anything beyond the most recent MAX_SNAPSHOTS. Only the script's relevant structure is shown below — the full code is on the Processor, not transcribed here.

#!/usr/bin/env python
import boto3

MAX_SNAPSHOTS = 2   # keep only the last 2 per volume

ec2 = boto3.resource('ec2')

for v in ec2.volumes.all():
    v.create_snapshot()
    snapshots = list(v.snapshots.all())
    if len(snapshots) > MAX_SNAPSHOTS:
        snap_sorted = sorted(
            [(s.id, s.start_time, s) for s in snapshots],
            key=lambda k: k[1]
        )
        for s in snap_sorted[:-MAX_SNAPSHOTS]:
            print("Deleting snapshot", s[0])
            s[2].delete()

The script also creates a snapshot on each run before pruning. That is the source of the new snap-080e659fbfaea38f1 visible in the post-execution list.

Before / after retention. The pre-run list has three snapshots (snap-0eeb6..., snap-0c318..., snap-0dc06...). After python3.8 snapshotter_v2.py, two are deleted and the post-run list shows the survivors: the oldest kept one plus the new snapshot the script itself took.
more snapshotter_v2.py showing boto3 code, then describe-snapshots before (3 IDs), then python3.8 snapshotter_v2.py with 'Deleting snapshot' messages, then describe-snapshots after (2 IDs)
Snapshot mechanics that matter: EBS snapshots are incremental — only changed blocks since the last snapshot are stored — but each one is restorable as a complete volume. Deleting an old snapshot is safe; AWS re-balances the underlying blocks so more recent snapshots remain whole.

Task 4 — Challenge: S3 sync with versioning

Switch hosts: this task runs on the Processor instance (which now has the S3BucketAccess role). The goal is to sync a local files/ directory with s3://superbucket3k/, delete a file locally, propagate the delete to S3, and then recover the deleted object using S3 versioning.

Sample files + enable versioning

The sample files.zip was already pre-staged on the Processor. After unzip files.zip the working directory contained:

$ unzip files.zip
Archive:  files.zip
 inflating: files/file1.txt
 inflating: files/file2.txt
 inflating: files/file3.txt

$ ls
cronjob  files  files.zip  get-pip.py  snapshotter_v2.py

$ ls files
file1.txt  file2.txt  file3.txt

Versioning was enabled on the bucket before any sync — this is the critical ordering, because objects uploaded while versioning is off cannot be recovered later.

aws s3api put-bucket-versioning \
  --bucket superbucket3k \
  --versioning-configuration Status=Enabled

The command returns nothing on success, which is correct AWS CLI behavior for idempotent configuration calls.

Initial sync

A plain aws s3 sync uploaded the three files. aws s3 ls confirmed the bucket state.

$ aws s3 sync files s3://superbucket3k/files/
upload: files/file1.txt to s3://superbucket3k/files/file1.txt
upload: files/file2.txt to s3://superbucket3k/files/file2.txt
upload: files/file3.txt to s3://superbucket3k/files/file3.txt

$ aws s3 ls s3://superbucket3k/files/
2026-05-15 06:12:49      30318 file1.txt
2026-05-15 06:12:49      43784 file2.txt
2026-05-15 06:12:49      96675 file3.txt

Delete locally · propagate with --delete

file1.txt was removed from the local files/ directory and a second sync was run with the --delete flag. Without --delete, aws s3 sync is upload-only and would have left the file in S3.

$ rm files/file1.txt
$ ls files
file2.txt  file3.txt

$ aws s3 sync files s3://superbucket3k/files/ --delete
delete: s3://superbucket3k/files/file1.txt

$ aws s3 ls s3://superbucket3k/files/
2026-05-15 06:12:49      43784 file2.txt
2026-05-15 06:12:49      96675 file3.txt
Important: aws s3 ls only shows the latest version of each key. Because versioning is enabled, the bucket has not actually lost file1.txt — the delete added a delete marker as the newest version. The original object is still there, hidden behind it.

Inspect versions · list the delete marker

list-object-versions returns two parallel arrays. Versions[] contains the actual content versions; DeleteMarkers[] contains the tombstone entries. After the propagated delete, file1.txt has one of each:

aws s3api list-object-versions \
  --bucket superbucket3k \
  --prefix files/file1.txt
The recovery proof. The bucket has both the original content version (VersionId: xyHef1ZSWB5VCgi4HR6917ssBs2uWoYh) and the delete marker (VersionId: F0kqOy0eIMZPQ3DCkWE8ThPR28YN7T1I). The delete marker has IsLatest: true; the real version has IsLatest: false.
list-object-versions output showing the Versions block with VersionId xyHef1ZSWB5VCgi4HR6917ssBs2uWoYh and the DeleteMarkers block with VersionId F0kqOy0eIMZPQ3DCkWE8ThPR28YN7T1I

Captured VersionIds from this run, used in the next step:

Type VersionId Effect
version xyHef1ZSWB5VCgi4HR6917ssBs2uWoYh Original content of file1.txt. Recoverable.
marker F0kqOy0eIMZPQ3DCkWE8ThPR28YN7T1I Tombstone created by sync --delete. Hides the version but does not destroy it.

Recover the previous version · re-sync

There is no single "restore" call in S3; the workflow is to download the previous version by VersionId straight to the original local path, then sync again so the bucket sees a new latest version.

$ aws s3api get-object \
  --bucket superbucket3k \
  --key files/file1.txt \
  --version-id xyHef1ZSWB5VCgi4HR6917ssBs2uWoYh \
  files/file1.txt

$ ls files
file1.txt  file2.txt  file3.txt

$ aws s3 sync files s3://superbucket3k/files/
upload: files/file1.txt to s3://superbucket3k/files/file1.txt

$ aws s3 ls s3://superbucket3k/files/
2026-05-15 06:18:12      30318 file1.txt
2026-05-15 06:12:49      43784 file2.txt
2026-05-15 06:12:49      96675 file3.txt

Notice the new LastModified on file1.txt: 06:18:12 instead of the original 06:12:49. The bucket now has three "live" objects again, plus the historical version chain underneath.

Recovery + re-sync. get-object returns metadata (VersionId, ContentLength: 30318, ETag, ServerSideEncryption: AES256). After it, ls files shows the three files locally and aws s3 sync re-uploads file1.txt.
get-object output with VersionId and metadata, followed by ls files (three files), aws s3 sync uploading file1.txt, and ls s3 showing the three files in the bucket

Key concepts

Three ideas that the lab cements through hands-on commands rather than theory.

Incremental snapshots

Each EBS snapshot is a point-in-time, restorable view of the whole volume, but internally only changed blocks are stored after the first one. Deleting old snapshots is safe — AWS re-references blocks so the surviving ones remain complete. That is what makes a retention policy like MAX_SNAPSHOTS=2 reasonable.

aws s3 sync · with vs. without --delete

Plain sync is one-way add/update: it only uploads new or changed files. With --delete, it also removes objects from the destination that no longer exist at the source — making the bucket a true mirror of the local directory. On a versioned bucket, that "remove" is actually a delete marker.

Delete markers vs. real deletes

With versioning enabled, every DELETE against an object just stamps a new "version" of type delete marker as the latest. The previous version stays intact and is recoverable by its VersionId. The only way to actually erase data is to delete the underlying version, not the key.

Instance profile > static keys

Attaching S3BucketAccess to the Processor as an instance profile lets the CLI fetch short-lived credentials from the EC2 metadata service. No aws configure, no keys on disk, no rotation logic. Every AWS CLI call in Tasks 3–4 used those temporary creds.

S3 versioning lifecycle (this lab in 4 states)

State 1 After first sync: file1.txt exists with a single version. IsLatest: true.
State 2 After rm + sync --delete: same content version still exists (IsLatest: false) plus a new delete marker (IsLatest: true).
State 3 After get-object --version-id: file restored locally. Bucket unchanged yet.
State 4 After final sync: new content version pushed (IsLatest: true), delete marker pushed down, original version still in the chain.

Command reference

Quick scan of every CLI verb used in this lab, grouped by phase.

Phase Command Purpose
EBS discovery aws ec2 describe-instances --filter ... --query ... Resolve VolumeId / InstanceId by tag.
EBS lifecycle aws ec2 stop-instances · wait instance-stopped Quiesce the volume before snapshotting.
EBS snapshot aws ec2 create-snapshot --volume-id ... Take a point-in-time snapshot. Async — returns pending.
EBS wait aws ec2 wait snapshot-completed --snapshot-id ... Block until the snapshot is 100% available.
EBS inspection aws ec2 describe-snapshots --filters "Name=volume-id,Values=..." List snapshots associated with a volume.
Scheduling crontab cronjob · crontab -r Install / remove the recurring snapshot job.
S3 setup aws s3api put-bucket-versioning --versioning-configuration Status=Enabled Activate versioning so deletes become recoverable.
S3 sync aws s3 sync <local> s3://<bucket>/<prefix>/ [--delete] One-way mirror; --delete propagates local removals.
S3 listing aws s3 ls s3://<bucket>/<prefix>/ Only shows the latest version of each key.
S3 versioning aws s3api list-object-versions --prefix ... Lists both Versions[] and DeleteMarkers[].
S3 recovery aws s3api get-object --version-id ... <out> Download a specific historical version of a key.