AWS re/Start Lab · EC2 + CLI

Managing Resources with Tagging

Inventory, modify, stop and terminate Amazon EC2 instances in bulk based on tags, using the AWS CLI with JMESPath queries and pre-provided scripts (Bash and PHP) running from a CommandHost EC2 instance. The lab also exercises a tag-or-terminate compliance policy as the optional challenge.

Lab Summary

Connected via SSH to the CommandHost EC2 instance and used the AWS CLI to inspect, filter and reshape EC2 metadata using JMESPath. Bumped the Version tag on all development instances via a small Bash script, then used stopinator.php (AWS SDK for PHP) to stop and start instances by tag. The challenge added a tag-or-terminate policy: instances without an Environment tag were terminated by terminate-instances.php.

JMESPath scaling

From a single field (InstanceId) up to a multi-field projection with aliases and tag lookup, then a double tag filter.

Tag-based lifecycle

stopinator.php stops or starts every instance that matches a tag=value filter across all regions.

Compliance enforcement

Diff between "all instances in subnet" and "instances with tag X" produces the set to terminate. Same logic underpins many FinOps and governance tools.

Scenario

A VPC with a CommandHost in the public subnet and 8 Linux instances in a private subnet. The private instances carry three custom tags that drive the entire lab.

Tag Possible values Purpose
Project ERPSystem, Experiment1 Which project the instance belongs to.
Version 1.0 (initial) Version of the project the instance runs.
Environment development, staging, production Lifecycle environment. Drives stop/start and tag-or-terminate logic.
Region: The lab runs in us-west-2. All commands and scripts are executed from the CommandHost over SSH.

Task 1 — Inspect and modify tags from the AWS CLI

Progressive JMESPath: start with raw filters, narrow output, add aliases, look up tag values, and finally apply a double tag filter. Then bulk-update a tag with a Bash script.

01

Filter by tag, then trim the output with --query

  • The unfiltered describe-instances output is overwhelming — most fields are not relevant for an inventory by tag.
  • --filter filters server-side by tag; --query reshapes the JSON client-side using JMESPath.
  • First version: list only InstanceId for every instance with Project=ERPSystem.
CommandHost · AWS CLI
# Filter by tag and return only the instance IDs
aws ec2 describe-instances \
  --filter "Name=tag:Project,Values=ERPSystem" \
  --query 'Reservations[*].Instances[*].InstanceId'

The JMESPath expression Reservations[*].Instances[*].InstanceId uses wildcards to walk every reservation and every instance, projecting only that single field.

02

Add multi-field projection with aliases

  • JMESPath supports {Alias:Field} syntax to rename fields in the output.
  • Useful for building human-readable inventories where the raw schema is too noisy.
CommandHost · AWS CLI
# Project ID and Availability Zone, renamed
aws ec2 describe-instances \
  --filter "Name=tag:Project,Values=ERPSystem" \
  --query 'Reservations[*].Instances[*].{ID:InstanceId,AZ:Placement.AvailabilityZone}'
03

Look up the value of a specific tag with a filter expression

  • EC2 tags are returned as an array of {Key, Value} objects, so a direct projection on Tags.Project does not work.
  • The pattern Tags[?Key==`Project`] | [0].Value filters the array to the element whose key is Project, pipes the single-element result to [0], and returns its Value.
  • The same pattern is repeated for Environment and Version to build a full inventory row.
CommandHost · AWS CLI
# Inventory: ID, AZ, Project, Environment, Version
aws ec2 describe-instances \
  --filter "Name=tag:Project,Values=ERPSystem" \
  --query 'Reservations[*].Instances[*].{
    ID:InstanceId,
    AZ:Placement.AvailabilityZone,
    Project:Tags[?Key==`Project`]      | [0].Value,
    Environment:Tags[?Key==`Environment`] | [0].Value,
    Version:Tags[?Key==`Version`]      | [0].Value
  }'
04

Add a second --filter for an AND condition

  • Passing two Name=tag:... filters in the same command performs an AND: only instances matching both tags are returned.
  • Here, the goal is the intersection of Project=ERPSystem and Environment=development — the development boxes of the ERP system.
CommandHost · AWS CLI
# Double tag filter — ERPSystem dev only
aws ec2 describe-instances \
  --filter "Name=tag:Project,Values=ERPSystem" \
            "Name=tag:Environment,Values=development" \
  --query 'Reservations[*].Instances[*].{
    ID:InstanceId,
    AZ:Placement.AvailabilityZone,
    Project:Tags[?Key==`Project`]      | [0].Value,
    Environment:Tags[?Key==`Environment`] | [0].Value,
    Version:Tags[?Key==`Version`]      | [0].Value
  }'
05

Bulk-update the Version tag with a Bash script

  • The script change-resource-tags.sh uses the same double-filter query but with --output text so the IDs can be piped directly into create-tags.
  • aws ec2 create-tags either creates a new tag or overwrites an existing one — here it bumps Version from 1.0 to 1.1 on every ERP-dev instance in a single call.
  • This is the simplest practical example of the pattern query → pipe IDs → act in bulk.
change-resource-tags.sh
#!/bin/bash

# Collect IDs of all ERPSystem development instances
ids=$(aws ec2 describe-instances \
  --filter "Name=tag:Project,Values=ERPSystem" \
            "Name=tag:Environment,Values=development" \
  --query 'Reservations[*].Instances[*].InstanceId' \
  --output text)

# Overwrite Version tag to 1.1 on every collected instance
aws ec2 create-tags --resources $ids \
                   --tags 'Key=Version,Value=1.1'
Note on evidence: The Version 1.0 → 1.1 change ran successfully against the lab fleet, but no before/after screenshot was captured. The verification command from the lab guide (aws ec2 describe-instances --filter "Name=tag:Project,Values=ERPSystem" --query '...Version:Tags[?Key==`Version`] | [0].Value') confirms the tag flips only on the development boxes; the others remain at 1.0.

Task 2 — Stop and start instances by tag with stopinator.php

A pre-provided PHP script that uses the AWS SDK for PHP to apply a stop/start lifecycle to every instance matching a tag filter, across all regions.

01

How the script works

  • Lives under ~/aws-tools/stopinator.php on the CommandHost.
  • Iterates over every AWS region, calls Ec2::DescribeInstances() with a tag filter, then stops every matching instance with Ec2::StopInstances().
  • Two arguments:
    • -t "Name=Value;Name=Value" — tag filter (semicolon-separated pairs). If omitted, stops every running instance in the account.
    • -s — switch to start mode instead of stop.
  • Use case: shut down the dev environment at the end of the day, start it up the next morning.
02

Stop all ERP-dev instances

The script reports each region it scans and lists the identified instances before issuing the stop call:

CommandHost · stopinator.php (stop)
cd ~/aws-tools
./stopinator.php -t"Project=ERPSystem;Environment=development"

# Output (abbreviated):
#   Region is us-east-1
#     No instances to stop in region
#   Region is us-west-2
#     Identified instance i-0798b4e50e2b64eae
#     Identified instance i-0c4838001b8a28add
#   Stopping all identified instances...
03

Restart the same set with -s

  • Same tag filter, plus the -s flag to switch to start mode.
  • No screenshot was captured of the restart; visible behaviour mirrors the stop call but with StartInstances in place of StopInstances and the instances eventually returning to Running in the console.
CommandHost · stopinator.php (start)
./stopinator.php -t"Project=ERPSystem;Environment=development" -s

Task 3 — Challenge: tag-or-terminate

Policy: any instance in the private subnet without an Environment tag is considered non-compliant and must be terminated. The provided terminate-instances.php implements that policy as a set difference.

01

Script logic in three blocks

  • List tagged instances. describeInstances() with a filter for the existence of the Environment tag (any value). IDs go into a hash table.
  • Diff against the subnet. Iterate over every instance in the target subnet; if its ID is not in the tagged hash, add it to a "to terminate" list.
  • Terminate. A single Ec2::terminateInstances() call with the full ID list.
Pattern: the script is a concrete example of the broader "compliance-by-difference" pattern — define the compliant set with a filter, list the full population, and act on the delta.
02

Preparing the test fleet

  • In the EC2 console, removed the Environment tag from two private instances (Add/Edit Tags → delete the row → Save).
  • This is the only way to create non-compliance in the lab, since every instance ships with the tag set.
  • Copied the region (AZ minus the trailing letter) and the private subnet ID for the script arguments.
03

Run the script and verify

CommandHost · terminate-instances.php
./terminate-instances.php -region us-west-2 \
                          -subnetid subnet-021318b87f05f11aa

Key Learnings

JMESPath, in five strokes

[*] — wildcard projection over arrays of objects.
{Alias:Field} — rename or reshape fields in the output.
[?Key==`X`] — filter expression on array elements.
| — pipe one expression into another (e.g. [?...] | [0].Value).
Multiple --filter arguments combine as AND, server-side.

Output format matters

JSON is fine for humans reading inventories; --output text is what you want when piping IDs into other AWS CLI calls. The change-resource-tags.sh script is a one-liner because of that single flag — switch it back to JSON and the create-tags --resources call breaks immediately.

Tag-based automation is the foundation of FinOps tooling

stopinator.php is a toy, but the pattern it shows — sweep all regions, filter by tag, apply a lifecycle action — is exactly what production tools like AWS Instance Scheduler and Cloud Custodian implement. Once tags are reliable, scheduling, right-sizing and compliance enforcement are all variations of the same script.

Compliance by set difference

The tag-or-terminate challenge is the simplest expression of a compliance policy: compliant set (instances with required tag) minus full set (instances in subnet) equals violation set. The same shape generalises to "untagged volumes", "security groups without owners", "S3 buckets without a CostCenter". The lifecycle action at the end is what varies.