# Ephemera — Network foundation: VPC + subnets + ACM cert (AWS CLI)

> Self-executing Markdown. The **upstream every other AWS plan leans on**: a VPC, public/private
> subnets, and a valid TLS certificate, published to **SSM Parameter Store** so consumers find them by a
> stable path. The cloud is the source of truth; this file is intent + write-back ledger.

> **Provides** `vpc(${VPC_ID})`, `subnets(public+private)`, `cert(${CERT_ARN})` — published to SSM at
> `/network/vpc`, `/network/subnet/public/1a|1b`, `/network/subnet/private/1a|1b`, `/network/cert/${AWS_REGION}`.
> `ecs-cluster.aws.md` **Requires** all of these. **Requires** (cert path only, when `DNS_PROVIDER=cloudflare`)
> `zone(example.com)` from `domain.cloudflare.md` — example.com is **authoritatively on Cloudflare**
> (`anna`/`fred.ns.cloudflare.com`, confirmed by `dig NS`), so ACM DNS-validation records go in **Cloudflare**,
> not Route 53. The `route53` branch (and `domain.aws.md`) remains for domains still delegated to AWS.

---

## 🤖 Director prompt

Observe before acting; verify each step; **stop at 🔴/💥 for human go**; write realized ids/arns back into
Live State each step; **never delete a VPC/subnet this plan did not create** (the default VPC and any
shared VPC are off-limits to teardown). Verbs: `verify` (read-only), `apply` (create, gated), `teardown`
(destroy what *this plan* created, gated).

```
Legend  🟢 create · 🟡 config · 🔴 GATE (human go) · 💥 destructive (human go) · ⏳ wait · ✔ verify
```

## What you need, and why (read this if infra is new to you)

A container service on AWS needs three foundational things before anything else can attach:

- **VPC** — a private network (an IP range like `172.31.0.0/16`) that everything lives inside. AWS gives
  every account a **default VPC** per region; that's usually enough for a demo. A *custom* VPC is worth
  it when you want clean public/private separation.
- **Subnets** — slices of the VPC pinned to an Availability Zone. **Public** = has a route to an Internet
  Gateway (reachable from the internet — where the load balancer lives). **Private** = no direct internet
  route (where app containers *should* live, reaching out via a NAT). You want at least **two AZs** for
  the ALB. *How to tell them apart:* a subnet is public iff its route table has a `0.0.0.0/0 → igw-…`
  route (the `MapPublicIpOnLaunch` flag is a hint, the route table is the truth).
- **TLS certificate (ACM)** — proves `https://yourname` is really you. ACM is **regional**: an **ALB**
  can only attach a cert in **its own region**, while **CloudFront** can only use **us-east-1**. So a
  valid `us-east-1` cert (great for CloudFront) is **useless to a us-west-2 ALB** — always evaluate certs
  **per region**. A public ACM cert is **free** but must be **validated** (a DNS record in the
  authoritative zone — here **Cloudflare**) and **renewed**; an *expired* cert is as good as none.

**Don't know if you already have these?** You don't have to guess — run `verify` (or §1 below). It
discovers what exists and tells you which of the two modes you're in:

- **`use-existing`** — discover real ids and just publish them to SSM (no new infra). *This is the
  default when a usable VPC + 2 public subnets already exist.*
- **`create-new`** — stand up a minimal VPC + subnets + routing from scratch, then publish.

Either way the cert is handled separately, because a VPC can exist while its cert is missing or expired.

## Dry-run findings (2026-06-24, account <AWS_ACCOUNT_ID> — multi-region)

```
DNS (dig NS) :  example.com → anna.ns.cloudflare.com / fred.ns.cloudflare.com   ⇒ DNS_PROVIDER=cloudflare
CF zone      :  id <CF_ZONE_ID>  active  acct <CF_ACCOUNT_ID>
CF SSL mode  :  full  (NOT strict)  always_use_https=on   ← cloudflare-origin wants strict PER-HOSTNAME (see §3)
SSM /network/*: ALL RESOLVE → <VPC_ID>, <SUBNET_ID> (2a), <SUBNET_ID> (2b), cert d1208d38-…
VPCs         :  <VPC_ID>   172.31.0.0/16  DEFAULT  ← SSM points here
                <VPC_ID>…  172.16.0.0/16  custom (has public+private subnets across AZs)
Public subnets: default VPC has public subnets 2a/2b/2c/2d (SSM uses 2a+2b) ✅

CERTS (per region — ACM is regional!):
  us-west-2 (ALB region):
    *.example.com  EXPIRED 2022-04-13  inUse=0   ❌
    *.example.com  EXPIRED 2026-01-01  inUse=0   ❌   ← no usable ALB cert in-region
  us-east-1 (CloudFront only):
    *.example.com  ISSUED  2027-02-28  inUse=2   ✅ (CloudFront — NOT attachable to a us-west-2 ALB)
    *.example.com  ISSUED  2027-03-01  inUse=7   ✅ (CloudFront — likely the SPA)
    (+ *.scobot.net, mikesnicemice.com, *.qwizgames.com — other domains)
```

**Verdict:** VPC + subnets **met** (`use-existing`, default VPC). DNS is **Cloudflare** → `DNS_PROVIDER`
defaults to `cloudflare`; validation records and the service DNS record go there, not Route 53. The
**ALB cert is NOT met**: the valid `*.example.com` certs are **us-east-1** (CloudFront-only) and the
two us-west-2 certs are expired — so §3 must produce a fresh **us-west-2** cert (or use a Cloudflare
Origin cert; see §3). *Recommendation:* demo on the default VPC is fine; for private-subnet task
placement, repoint SSM at custom `<VPC_ID>…` (Input #2).

## Provisioning Inputs

| # | Question | Options (closed enum) | Default | Sets | Gates |
|---|----------|-----------------------|---------|------|-------|
| 1 | Network mode | `use-existing` / `create-new` | `use-existing` | `NETWORK_MODE` | §1 vs §2 |
| 2 | Which VPC (if use-existing & >1) | `default` / `<vpc-id>` | `default` | `VPC_ID` | §1 |
| 3 | Region (ALB cert region) | `us-west-2` / `us-east-1` / … | `us-west-2` | `AWS_REGION` | cert path, all ids |
| 4 | DNS provider (where validation + records live) | `cloudflare` / `route53` / `godaddy` / `manual` | `cloudflare` | `DNS_PROVIDER` | §3 validation, consumers' DNS records |
| 5 | Cert strategy | `cloudflare-origin` / `request-acm-dns` / `import` / `reuse-valid` | `cloudflare-origin` | `CERT_MODE` | §3 |
| 6 | Cert domain (free-text) | e.g. `*.example.com` | `*.example.com` | `CERT_DOMAIN` | §3 |
| 7 | Create private subnets + NAT? (create-new only) | `no` / `yes` | `no` | `WANT_PRIVATE` | §2 |

**Determinism — same answers ⇒ same cert ARN (never a duplicate).** ACM `request-certificate`/`import` are
*not* idempotent — a naive re-apply mints a fresh cert each time. So §3 keys every cert on a **deterministic
tag** `Name=ephemera-cert/${CERT_DOMAIN}/${AWS_REGION}` and **observes before acting**: it looks the cert up
by that tag and reuses it if found; only if absent does it produce one (then tag it). `reuse-valid` is made
unambiguous by a fixed tiebreak (the tagged cert; else the ISSUED, unexpired match with the latest
`NotAfter`). The resolved inputs are a closed enum, the tag is a pure function of them, and the lookup makes
re-runs converge — so the cert is as deterministic as the rest of the plan.

`DNS_PROVIDER` is provider-agnostic by design: `cloudflare`/`route53`/`godaddy` automate the record via
their API; **`manual`** prints the exact record and waits for the human to add it at whatever registrar
they use (GoDaddy UI, Namecheap, corporate DNS…), then verifies by `dig` — still deterministic, because the
record *content* is a pure function of the cert, only its placement is hand-done.

`cloudflare-origin` (the resolved choice) is lowest-maintenance: Cloudflare terminates public TLS at its
edge (Universal SSL, auto-renewed); the ALB carries a free 15-year **Cloudflare Origin CA cert** imported
into ACM for the edge→origin leg — no ACM public-cert renewal, and SSL mode **Full (strict)** end-to-end.

```yaml
# → written into Live State once resolved
resolved_inputs:
  network_mode: use-existing
  vpc_id:       default              # <VPC_ID>
  region:       us-west-2
  dns_provider: cloudflare           # example.com NS = *.ns.cloudflare.com (verified)
  cert_mode:    cloudflare-origin     # resolved 2026-06-24: CF edge TLS + Origin cert on ALB, no ACM renewal
  cert_domain:  "*.example.com"
  cert_tag:     "ephemera-cert/*.example.com/us-west-2"   # deterministic identity for observe-before-act reuse
  want_private: no
  resolved_by:  <human who confirmed>
  resolved_at:  2026-06-24
```

## Live State

```yaml
status:        not-created      # published template - run it to realize state
last_action:   dry-run discovery 2026-06-24 — see findings above
last_verified: SSM params resolve; ACM cert EXPIRED (negative assertion FAILED)
```

| key                | value |
|--------------------|-------|
| AWS_REGION         | `us-west-2` |
| ACCOUNT_ID         | `<AWS_ACCOUNT_ID>` |
| VPC_ID             | `<VPC_ID>` (default) |
| SUBNET_PUB_1A      | `<SUBNET_ID>` (us-west-2a, public) |
| SUBNET_PUB_1B      | `<SUBNET_ID>` (us-west-2b, public) |
| SUBNET_PRIV_1A/1B  | `—` (not wired; available in custom <VPC_ID>… if needed) |
| DNS_PROVIDER       | `cloudflare` (example.com NS = `*.ns.cloudflare.com`) |
| CF_ZONE_ID         | `<CF_ZONE_ID>` (example.com, active) |
| CF_ACCOUNT_ID      | `<CF_ACCOUNT_ID>` |
| CF_SSL_MODE        | `full` (discovered) — `cloudflare-origin` wants **strict, per-hostname** (§3) |
| CERT_ARN (us-west-2)| `<ARN>…/d1208d38-…` **EXPIRED — replace via §3** |
| valid certs (us-east-1)| `*.example.com` ISSUED→2027 (CloudFront-only; **not** ALB-attachable) |

| ✔ check                       | expected                              | observed (2026-06-24) | result |
|-------------------------------|---------------------------------------|-----------------------|--------|
| VPC resolvable                | a real `vpc-…`                        | <VPC_ID>          | ✅ |
| ≥2 public subnets, distinct AZ| 2a + 2b, route to IGW                 | <SUBNET_ID>/07813c62 | ✅ |
| SSM `/network/*` populated    | all paths resolve                     | resolve               | ✅ |
| DNS provider = authoritative  | `dig NS` matches `DNS_PROVIDER`       | Cloudflare            | ✅ |
| CF zone reachable + token ok  | zone `active`, token verifies         | id f1af47…, token active | ✅ |
| CF SSL strict for ALB hostname| Full(strict) on the service hostname  | zone-wide `full` (per-host strict not set) | ⚠️ |
| **cert ISSUED & not expired, in us-west-2** | `ISSUED`, future `NotAfter`, region=ALB | **EXPIRED (us-west-2); valid only in us-east-1** | ❌ |

## 0. Variables

```bash
export AWS_REGION="us-west-2"
export ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"
export CERT_DOMAIN="*.example.com"
```

## Dependency frontier

```
NETWORK_MODE? ─┬─ use-existing ─> §1 discover (pick VPC, classify subnets by route table) ─> publish SSM ─┐
               └─ create-new   ─> §2 VPC+IGW+subnets+routes (+NAT if WANT_PRIVATE) ─────────> publish SSM ─┤
                                                                                                           ├─> Provides{vpc,subnets}
DNS_PROVIDER zone ─> §3.0 evaluate (per region) ─> §3 cert (acm-dns / cloudflare-origin / import / reuse) ─┴─> Provides{cert}
```
Non-negotiable edges: **a public subnet is defined by its route table**, so classification reads routes,
not just the `MapPublicIpOnLaunch` flag; **ACM is regional** — evaluate/produce the cert in `AWS_REGION`
(a valid us-east-1 cert can't attach to a us-west-2 ALB); **DNS-validated issuance needs the
authoritative zone** — `Requires` `domain.cloudflare.md` when `DNS_PROVIDER=cloudflare`. Cert and VPC are
independent — a VPC can be present while the cert is expired (exactly today's state).

## 1. Discover existing network & publish to SSM (use-existing)  🟢🟡

```bash
# pick the VPC (default, or an explicit id from Provisioning Input #2)
VPC_ID="$([ "${VPC_ID:-default}" = default ] \
  && aws ec2 describe-vpcs --filters Name=isDefault,Values=true --query 'Vpcs[0].VpcId' --output text \
  || echo "$VPC_ID")"

# classify each subnet by its route table (public iff a 0.0.0.0/0 → igw-… route exists)
for s in $(aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC_ID --query 'Subnets[].SubnetId' --output text); do
  rt=$(aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=$s \
        --query 'RouteTables[0].Routes[?GatewayId!=`null`].GatewayId' --output text)
  az=$(aws ec2 describe-subnets --subnet-ids $s --query 'Subnets[0].AvailabilityZone' --output text)
  echo "$s $az $([ -n "$(echo "$rt" | grep -o igw-)" ] && echo PUBLIC || echo PRIVATE)"
done   # pick two PUBLIC subnets in distinct AZs → SUBNET_PUB_1A / 1B

# publish to the stable SSM paths consumers Require
aws ssm put-parameter --overwrite --type String --name /network/vpc --value "$VPC_ID"
aws ssm put-parameter --overwrite --type String --name /network/subnet/public/1a --value "$SUBNET_PUB_1A"
aws ssm put-parameter --overwrite --type String --name /network/subnet/public/1b --value "$SUBNET_PUB_1B"
```
```bash
# ✔ verify — the two published subnets really are public and in different AZs
aws ec2 describe-subnets --subnet-ids "$SUBNET_PUB_1A" "$SUBNET_PUB_1B" \
  --query 'Subnets[].AvailabilityZone' --output text   # two distinct AZs
```
> → Live State: VPC_ID, SUBNET_PUB_1A, SUBNET_PUB_1B

## 2. Stand up a new VPC (create-new)  🔴🟢

> 🔴 GATE — creating a network. Minimal, demo-grade: VPC + IGW + 2 public subnets (2 AZs) + route. Add
> private subnets + NAT only if `WANT_PRIVATE=yes` (NAT gateways are **billable hourly** — a real cost gate).

```bash
VPC_ID="$(aws ec2 create-vpc --cidr-block 10.20.0.0/16 \
  --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=ephemera-vpc},{Key=ManagedBy,Value=ephemera}]' \
  --query 'Vpc.VpcId' --output text)"
aws ec2 modify-<VPC_ID>ttribute --vpc-id "$VPC_ID" --enable-dns-hostnames
IGW_ID="$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text)"
aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID"
RT_ID="$(aws ec2 create-route-table --vpc-id "$VPC_ID" --query 'RouteTable.RouteTableId' --output text)"
aws ec2 create-route --route-table-id "$RT_ID" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID" >/dev/null
i=0; for az in a b; do
  SUB="$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block "10.20.$i.0/24" \
    --availability-zone "${AWS_REGION}${az}" --query 'Subnet.SubnetId' --output text)"
  aws ec2 modify-<SUBNET_ID>ttribute --subnet-id "$SUB" --map-public-ip-on-launch
  aws ec2 associate-route-table --route-table-id "$RT_ID" --subnet-id "$SUB" >/dev/null
  aws ssm put-parameter --overwrite --type String --name "/network/subnet/public/1${az}" --value "$SUB"
  i=$((i+1))
done
aws ssm put-parameter --overwrite --type String --name /network/vpc --value "$VPC_ID"
# WANT_PRIVATE=yes → repeat with private subnets, a NAT gateway (💥 billable), and a private route table.
```
```bash
# ✔ verify — public subnets route to the IGW
aws ec2 describe-route-tables --filters Name=route-table-id,Values="$RT_ID" \
  --query 'RouteTables[0].Routes[?DestinationCidrBlock==`0.0.0.0/0`].GatewayId' --output text   # igw-…
```
> → Live State: VPC_ID, IGW_ID, RT_ID, SUBNET_PUB_1A, SUBNET_PUB_1B

## 3.0 Deterministic cert lookup (read-only — observe before acting)  ✔

> ACM is regional, so evaluate **`AWS_REGION` only** (a valid us-east-1 cert is CloudFront-only and can't
> attach here). The cert has **one deterministic identity** — the tag below — so a re-apply finds and
> reuses it instead of minting a duplicate. This is what makes the cert a pure function of the inputs.

```bash
export CERT_TAG="ephemera-cert/${CERT_DOMAIN}/${AWS_REGION}"   # deterministic identity (pure fn of inputs)

# (a) is our tagged cert already here? → reuse it, skip §3 entirely (idempotent)
CERT_ARN="$(aws resourcegroupstaggingapi get-resources --region "$AWS_REGION" \
  --resource-type-filters acm:certificate \
  --tag-filters Key=Name,Values="$CERT_TAG" \
  --query 'ResourceTagMappingList[0].ResourceARN' --output text 2>/dev/null)"

# (b) else, for reuse-valid: deterministic pick = ISSUED+unexpired match with the LATEST NotAfter
if [ -z "$CERT_ARN" ] || [ "$CERT_ARN" = "None" ]; then
  for a in $(aws acm list-certificates --region "$AWS_REGION" \
              --query "CertificateSummaryList[?DomainName=='${CERT_DOMAIN}'].CertificateArn" --output text); do
    aws acm describe-certificate --region "$AWS_REGION" --certificate-arn "$a" \
      --query 'Certificate.[CertificateArn,Status,NotAfter]' --output text   # filter Status==ISSUED, sort -k3, take last
  done
fi
# 2026-06-24 us-west-2: no tagged cert, and the only domain matches are EXPIRED → §3 must produce one.
```

## 3. Certificate for the ALB — produce only if §3.0 found none  🔴🟢

> 🔴 GATE. Run **only when §3.0 left `CERT_ARN` empty**. Each branch **tags the cert with `$CERT_TAG`** so
> the next apply reuses it (no duplicates). Validation/record placement is routed by `DNS_PROVIDER` below.

```bash
[ -n "$CERT_ARN" ] && [ "$CERT_ARN" != None ] && echo "reusing $CERT_ARN — skip §3" || case "$CERT_MODE" in

  cloudflare-origin)  # RESOLVED CHOICE. CF terminates public TLS at the edge; ALB holds a CF Origin cert.
    # CF_ZONE_ID=<CF_ZONE_ID> (discovered); token in $CLOUDFLARE_API_TOKEN.
    # 1. Create a 15-yr Cloudflare Origin CA cert for $CERT_DOMAIN (CF API /certificates with a CSR, or dashboard) → origin.pem/.key
    # 2. Import into ACM in AWS_REGION (tagged for deterministic reuse):
    CERT_ARN="$(aws acm import-certificate --region "$AWS_REGION" \
      --certificate fileb://origin.pem --private-key fileb://origin.key \
      --tags Key=Name,Value="$CERT_TAG" --query CertificateArn --output text)"
    # 3. SSL mode: the zone is currently 'full' (discovered). Do NOT flip the whole zone to strict — that's
    #    zone-wide and would break any sibling origin not presenting a CF-trusted cert (SPA, other subdomains).
    #    Set strict for THIS hostname only, via a Configuration Rule (🔴 blast-radius decision):
    #      POST /zones/$CF_ZONE_ID/rulesets ... action=set_config { ssl: "strict" }  when host == $SERVICE_HOST
    #    'full' is the acceptable fallback (edge→origin encrypted; origin cert not validation-enforced).
    # 4. The service DNS record (ecs-service §10) is PROXIED (orange). No ACM renewal ever.
    ;;

  request-acm-dns)    # free public ACM cert in AWS_REGION, DNS-validated at $DNS_PROVIDER
    CERT_ARN="$(aws acm request-certificate --region "$AWS_REGION" --domain-name "$CERT_DOMAIN" \
      --validation-method DNS --tags Key=Name,Value="$CERT_TAG" --query CertificateArn --output text)"
    read -r VN VT VV < <(aws acm describe-certificate --region "$AWS_REGION" --certificate-arn "$CERT_ARN" \
      --query 'Certificate.DomainValidationOptions[0].ResourceRecord.[Name,Type,Value]' --output text)
    # place the validation record per provider (record CONTENT is identical & deterministic; only the API differs):
    case "$DNS_PROVIDER" in
      cloudflare) curl -fsS -X POST "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \
                    -H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json" \
                    --data "{\"type\":\"$VT\",\"name\":\"$VN\",\"content\":\"$VV\",\"proxied\":false}" ;;  # grey/DNS-only
      route53)    aws route53 change-resource-record-sets --hosted-zone-id "$R53_ZONE_ID" --change-batch "{
                    \"Changes\":[{\"Action\":\"UPSERT\",\"ResourceRecordSet\":{\"Name\":\"$VN\",\"Type\":\"$VT\",
                    \"TTL\":300,\"ResourceRecords\":[{\"Value\":\"\\\"$VV\\\"\"}]}}]}" ;;
      godaddy)    curl -fsS -X PATCH "https://api.godaddy.com/v1/domains/${CERT_DOMAIN#\*.}/records" \
                    -H "Authorization: sso-key ${GODADDY_KEY}:${GODADDY_SECRET}" -H "Content-Type: application/json" \
                    --data "[{\"type\":\"$VT\",\"name\":\"${VN%%.${CERT_DOMAIN#\*.}*}\",\"data\":\"$VV\",\"ttl\":600}]" ;;
      manual)     echo "ADD THIS RECORD AT YOUR DNS HOST, THEN PRESS ENTER:  $VT  $VN  →  $VV"; read -r _ ;;
    esac
    aws acm wait certificate-validated --region "$AWS_REGION" --certificate-arn "$CERT_ARN" ;;  # ⏳

  import)             # bring-your-own cert
    CERT_ARN="$(aws acm import-certificate --region "$AWS_REGION" \
      --certificate fileb://cert.pem --private-key fileb://key.pem --certificate-chain fileb://chain.pem \
      --tags Key=Name,Value="$CERT_TAG" --query CertificateArn --output text)" ;;

  reuse-valid)        echo "§3.0 found no tagged/valid cert in $AWS_REGION — pick another CERT_MODE"; exit 1 ;;
esac
aws ssm put-parameter --overwrite --type String --name "/network/cert/${AWS_REGION}" --value "$CERT_ARN"
```
```bash
# ✔ verify — ISSUED/valid and NOT expired, IN AWS_REGION (negative assertion: SSM no longer points at an expired arn)
aws acm describe-certificate --region "$AWS_REGION" --certificate-arn "$CERT_ARN" \
  --query 'Certificate.{Status:Status,NotAfter:NotAfter}' --output json   # Status=ISSUED, NotAfter in the future
```
> → Live State: CERT_ARN; flip the cert ✔ row to ✅

## Update (idempotent reconcile)

- Re-point SSM at a different VPC (e.g. the custom `<VPC_ID>…` for private-subnet placement) → re-run §1
  with `VPC_ID=<that>`; consumers pick up the change on their next `verify`.
- Cert nearing expiry but ACM not auto-renewing (validation CNAME removed from Cloudflare) → re-add the
  validation CNAME in Cloudflare (DNS-only/grey-cloud); ACM renews a still-validated cert automatically.
  `cloudflare-origin` certs don't expire for 15 years and need no ACM renewal at all.

## Teardown (observe-first, resumable)  💥

> **Only tears down what this plan created.** `use-existing` created *no* VPC/subnets — teardown there is
> just removing the SSM pointers. **Never delete the default VPC or a shared VPC.**

```bash
# use-existing: drop only the SSM pointers (leave the borrowed VPC/subnets/cert intact)  💥(SSM only)
for p in /network/vpc /network/subnet/public/1a /network/subnet/public/1b /network/cert/${AWS_REGION}; do
  aws ssm delete-parameter --name "$p" 2>/dev/null || true
done
# create-new: reverse §2 — subnets, route table, detach+delete IGW, delete VPC (after consumers gone)  💥
# cert (if this plan requested it): aws acm delete-certificate --certificate-arn "$CERT_ARN"  💥
```
```bash
# ✔ teardown verify
aws ssm get-parameter --name /network/vpc 2>&1 | grep -q ParameterNotFound && echo ssm-cleared
```
> → Live State: set `status: gone` (create-new) or `status: not-created` (use-existing, pointers removed).

## Deliberately not included

- **The default VPC itself** — borrowed in `use-existing`, never created or destroyed here.
- **NAT gateway by default** — billable hourly; only via `WANT_PRIVATE=yes`. A demo with public-subnet
  Fargate tasks (`assignPublicIp=ENABLED`) needs no NAT.
- **VPC peering / Transit Gateway / VPN / flow logs** — out of scope; author a follow-on plan.
- **The DNS zone** — a `Requires` on `domain.cloudflare.md` (example.com is on Cloudflare), used by
  §3 for DNS validation / the proxied record; `domain.aws.md` only for domains still on Route 53.
