# Ephemera — Shared dependency layer on AWS (Lambda Layers + aws CLI)

> Self-executing Markdown. The **AWS binding** of the *lambda-layer* (shared-code packaging) intent — build a
> dependency/shared-code archive once, publish it as a versioned Lambda Layer, and attach it to many functions
> so their own zips stay small and the dependency is versioned independently. NOT durable execution, NOT a
> runtime — a **packaging artifact**. The cloud is the source of truth; this file is intent + ledger + audit.

> **Provides / Requires**: **Provides** `lambda-layer(LAYER_NAME, LayerVersionArn)` — a consumer function plan
> ([`service.aws.md`](./service.aws.md) via its `BIND_LAYER` knob, [`task-runner.aws.md`](./task-runner.aws.md))
> **Requires** it and attaches the ARN with `--layers`; it **discovers** the latest version by layer name at
> bind time (`list-layer-versions`). **Requires** nothing upstream.

---

## 🤖 Director prompt

Observe before acting; verify each step before advancing; stop at 🔴/💥 for human go; write realized values
back into Live State. **`publish-layer-version` is non-idempotent** — every call mints a *new* version (the ACM
`request-certificate` lesson). So `apply` **reuses the latest existing version** by the layer's deterministic
name; publishing a new version is an explicit `update` (you bumped the dependency). Layers are **append-only and
immutable** — there is no in-place edit, and old versions linger until teardown. IAM (the acceptance harness's
throwaway role) needs `--no-session` under a credential broker.

> **Candor:** **dogfooded live 2026-07-05, BOTH runtime branches** (python/`requests` · nodejs/`lodash`;
> us-west-2, ~$0, everything torn down): the false→true attach flip, the detach **round-trip** (bare
> `--layers` = detach-all → import fails again), version-stability on re-apply (no mint), the reuse guard's
> cross-runtime refusal, and the bump path (publish → `:2` → `max_by` re-discovery) all passed against a
> real account. CLI v2 gotchas confirmed live: `--zip-file fileb://…` and `invoke --cli-binary-format
> raw-in-base64-out`. Mark further drift in Live State as you run.

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

## What you need, and why  *(read if Lambda Layers are new to you)*

A **layer** is a zip of libraries/shared code that Lambda unpacks into `/opt` alongside your function at
runtime. It exists to solve one problem: **don't bundle the same dependency into every function**. You publish
`requests` (or your shared utils) once as a layer, attach its ARN to N functions, and each function's own zip
holds only *your* handler. A function can attach up to **5 layers**; the combined **unzipped** size of the
function + all its layers must stay under **250 MB** (the classic "why does my function exceed the limit" trap
is a fat layer — e.g. headless-chromium). Layers are **region-scoped** and each version has an immutable ARN
ending in `:<version>`.

- **`aws` CLI v2, authenticated** (`aws sts get-caller-identity` succeeds) + **`zip`**.
- **`RUNTIME=python`** needs **`pip`/`python3`**; **`RUNTIME=nodejs`** needs **`npm`/`node`**.
- The acceptance harness creates a throwaway IAM role → under a broker its create/delete ride `--no-session`.

## Intent

Package a shared dependency (or common code) as a **versioned, attachable artifact** so many functions reuse it
without each re-bundling it. The whole point is the *seam*: this plan Provides an ARN; function plans attach it.
Its correctness is therefore proven by attachment — **a function whose own zip does not contain the package can
still `import` it once the layer is attached, and can no longer import it once the layer is removed.**

**Acceptance contract** (this packaging intent; no sibling binding today — Cloudflare Workers bundle deps at
build time, so there is no `lambda-layer.cloudflare.md`; this is an AWS-shaped intent):
1. `publish-layer-version` returns a versioned `LayerVersionArn`; `get-layer-version` reports the compatible
   runtime and a non-zero `CodeSize` (the artifact is well-formed).
2. **Attachment proof (positive + negative).** A throwaway probe function whose own zip vendors **nothing**:
   - with **no** layer → invoking it reports `imported: false` (the dependency is genuinely absent), then
   - after attaching the layer → invoking it reports `imported: true` (the **layer** supplied the import).
   The false→true flip across attach is the assertion that earns this plan its keep.
3. **Determinism under a non-idempotent API.** Re-running `apply` does **not** mint a new version — it reuses the
   latest one for the deterministic `LAYER_NAME`. A new version appears only on an explicit `update` (bump).

## Provisioning Inputs

| # | Question | Options (closed enum) | Default | Sets | Gates |
|---|----------|-----------------------|---------|------|-------|
| 1 | Layer runtime family | `python` / `nodejs` | `python` | `RUNTIME` | §0 dir layout + `compatible-runtimes` + probe language |
| 2 | What to package | text — a pip/npm package name (must equal its import name — see §1 verify note) | `requests` | `PKG` | §1 (what gets vendored) + §3 (the import proved) |
| 3 | Layer noun | text — `[a-z0-9-]` | `shared-deps` | `LAYER` | the layer name (`${LAYER}-${ENV}`) |
| 4 | Environment | `dev` / `stg` / `uat` / `prod` | `dev` | `ENV` | name (`${LAYER}-${ENV}`) |

```yaml
# → written into Live State once resolved (the deterministic input to every step below)
resolved_inputs:
  runtime:     python        # python | nodejs
  pkg:         requests       # the pip/npm dependency vendored into the layer
  layer:       shared-deps
  env:         dev
  resolved_by: <human who confirmed>
  resolved_at: <timestamp>
```

> **Architecture.** This plan publishes with `--compatible-architectures x86_64 arm64` and vendors a **pure**
> package (the `requests` default is pure-Python), so either architecture works. A **compiled** dependency
> (numpy, pillow, anything with a native wheel) is arch-specific — build it on/for the target arch and narrow
> `--compatible-architectures` to match the consuming function, or the import fails at runtime. Named as a knob
> to reach for, not defaulted. ⚠ Observed live: even a "pure" package's **transitive** deps may ship platform
> wheels (requests → charset_normalizer's mypyc `.so`) — §1's `--platform manylinux2014_x86_64` pin is what
> makes the artifact host-independent; don't drop it.

## Tags & provenance (an AWS-internal asymmetry)

**Lambda layer versions are NOT taggable.** `aws lambda tag-resource` accepts only function / code-signing-config
/ event-source-mapping / capacity-provider ARNs — a layer version ARN is rejected, and layers do not appear in
`resourcegroupstaggingapi`. So unlike every other AWS resource in this fleet, the deliverable here carries **no
`ManagedBy=ephemera` tag**. Provenance degrades — exactly as it does on Cloudflare — to:

- **Naming convention** — `${LAYER}-${ENV}` is the "what manages this" signal (the only one available).
- **`--description`** — this plan stamps `ephemera · <plan> · <PlanVersion>` into the layer version description
  (the closest thing to a tag a layer has; visible in `get-layer-version`).

Consequence for teardown: ownership can only be **name-based**, not tag-verified — a weaker guarantee than the
tagged plans. Teardown trusts the deterministic name; it cannot prove a same-named layer was *ours*. This gap is
a portability insight (an AWS resource AWS won't let you tag), not a defect.

## Live State

```yaml
status:        not-created      # published template - run it to realize state
last_action:   dogfooded — python(requests) + nodejs(lodash) layers published, probe flips proven, bump→:2, all torn down + absence-verified
last_verified: 2026-07-05 — flip false→true→false(detach) · stability (re-apply reused :1, no mint) · reuse-guard cross-runtime refusal · bump→max_by picked :2 · IAM-propagation retry fired live
resolved_inputs: { runtime: python, pkg: requests, layer: shared-deps, env: dev }   # dogfood record — re-interview on next apply
```

| key              | value (filled on apply) |
|------------------|-------------------------|
| AWS_REGION       | `—` |
| LAYER_NAME       | `${LAYER}-${ENV}` |
| LAYER_VERSION_ARN| `—` (`…:layer:${LAYER}-${ENV}:<n>`) |
| VERSION          | `—` |

| ✔ check                          | expected                                                     | observed (2026-07-05 dogfood) | result |
|----------------------------------|--------------------------------------------------------------|----------|--------|
| layer version published          | `get-layer-version` → compatible runtime + `CodeSize > 0`    | py `:1` CodeSize=1089026 · js `:1`,`:2` | ✅ |
| import fails without the layer   | probe fn (no layer) → `imported: false` (negative baseline)  | both branches: `imported: false` | ✅ |
| import works with the layer      | same probe fn + layer → `imported: true`                     | py: `/opt/python/requests/__init__.py` · js: lodash via NODE_PATH | ✅ |
| apply is version-stable          | re-apply reuses latest version (no new version minted)       | re-ran §2 → reused `:1` | ✅ |
| detach round-trip (extra)        | bare `--layers` → `Layers[]` empty → import false again      | confirmed — the full flip reversed | ✅ |
| teardown leaves nothing          | all versions + probe fn/role absence-verified                | 6/6 gone (2 layers, 3 versions total) | ✅ |

## 0. Variables

```bash
set -euo pipefail
export AWS_REGION="${AWS_REGION:-us-west-2}"
export ENV="${ENV:-dev}"
export RUNTIME="${RUNTIME:-python}"          # python | nodejs
export PKG="${PKG:-requests}"                # pip/npm dependency to vendor (import name == package name)
export LAYER="${LAYER:-shared-deps}"
printf '%s' "$LAYER" | grep -Eq '^[a-z0-9-]+$' || { echo "LAYER must be [a-z0-9-]"; exit 1; }

LAYER_NAME="${LAYER}-${ENV}"
BUILD="/tmp/${LAYER_NAME}-build"
PLAN_VERSION="2026-07-05"
DESCRIPTION="ephemera · lambda-layer.aws.md · ${PLAN_VERSION} · ${PKG}"   # the only provenance a layer carries

# runtime → dir layout + Lambda runtime id (the layer's on-disk shape is runtime-specific)
if [ "$RUNTIME" = "python" ]; then
  LAMBDA_RUNTIME="python3.12"; LAYER_SUBDIR="python"          # /opt/python is on sys.path
  COMPAT_RUNTIMES="python3.12 python3.11"
else
  LAMBDA_RUNTIME="nodejs20.x"; LAYER_SUBDIR="nodejs/node_modules"   # /opt/nodejs/node_modules on require path
  COMPAT_RUNTIMES="nodejs20.x nodejs18.x"
fi
```

## Dependency frontier

```
§1 build artifact (runtime-specific dir + zip) ─> §2 publish-layer-version 🟢 (observe: reuse latest, else mint) ─> Provides LayerVersionArn
                                                                      └─> §3 acceptance: probe fn (no-layer ✖ → +layer ✔) ─> teardown probe harness
```
Non-negotiable edges: **the zip's internal directory must match the runtime** (`python/` vs
`nodejs/node_modules/`) or the runtime won't find the package; **publish before attach** (the ARN doesn't exist
until §2); the acceptance **negative must run before the positive** (create the probe with no layer, prove the
import is genuinely absent, *then* attach). Teardown reverses (delete every version).

## 1. Build the layer artifact  🟢  *(runtime-specific directory layout)*

```bash
rm -rf "$BUILD"; mkdir -p "$BUILD/layer/${LAYER_SUBDIR}"
if [ "$RUNTIME" = "python" ]; then
  # vendor the package INTO python/ (added to sys.path as /opt/python) — PINNED to the Lambda platform.
  # A bare pip on macOS vendors HOST wheels (observed live 2026-07-05: charset_normalizer arrived as a
  # cpython-310-darwin .so; the import survived only because that package degrades to pure-python — numpy
  # or pillow would not). The pinned form builds a correct artifact on any host. ⚠ --only-binary requires
  # every dep to ship a manylinux wheel (true for the default; a source-only dep needs a linux build host).
  python3 -m pip install --quiet --target "$BUILD/layer/python" \
    --platform manylinux2014_x86_64 --implementation cp --python-version 3.12 --only-binary=:all: "$PKG"
else
  # npm --prefix must be the nodejs/ dir itself — `--prefix .` from layer/ installs to the ZIP ROOT
  # (node_modules/ beside nodejs/, invisible to the runtime) while the empty nodejs/node_modules/ still
  # zips a directory entry, so a root-grep verify passes on the broken artifact (both halves observed
  # live 2026-07-05). cd in and anchor on $PWD.
  # ⚠ the package must SHIP CommonJS: an ESM-only package (e.g. uuid ≥v13) fails `require()` with
  # ERR_REQUIRE_ESM on nodejs20.x AND nodejs22.x (observed live — Lambda's node22 lacks require(esm)),
  # and dynamic import() cannot see NODE_PATH layers. Check the package's "main"/"exports" (lodash proven).
  ( cd "$BUILD/layer/nodejs" && npm install --silent --prefix "$PWD" "$PKG" )   # yields nodejs/node_modules/<pkg>/
fi
( cd "$BUILD/layer" && zip -q -r -X "$BUILD/layer.zip" . )          # -X: no extra file attrs (leaner, steadier)
```
```bash
# ✔ the PACKAGE landed at the runtime-specific path — not just "the dir exists" (an empty dir still zips
#   an entry, so a bare dir-grep is fail-open). Assumes $PKG equals its import/dir name (true for the
#   default `requests`; a dist whose import name differs — e.g. PyYAML→yaml — needs this grep adjusted).
#   grep WITHOUT -q: under §0's `pipefail`, grep -q exits at first match and a long unzip listing dies
#   SIGPIPE(141) → the ✔ silently fails on a GOOD artifact (observed live 2026-07-05).
unzip -l "$BUILD/layer.zip" | grep "${LAYER_SUBDIR}/${PKG}/" >/dev/null && echo "layer.zip built (${LAYER_SUBDIR}/${PKG}/ present)"
```
> → Live State: artifact built (local); not yet published.

## 2. Publish the layer version  🟢  *(non-idempotent — observe, reuse latest, else mint)*

> `publish-layer-version` mints a NEW version every call. So `apply` first asks "does a version already exist for
> this name?" and **reuses the latest** — re-running the plan does not sprawl versions. Publishing a new version
> is the `update` path (you bumped `PKG`). See Update.

```bash
# max_by, not LayerVersions[0] — the API documents no ordering, and [0] under CLI auto-pagination can emit
# one ARN per page; max_by is order-independent.
LATEST_ARN="$(aws lambda list-layer-versions --region "$AWS_REGION" --layer-name "$LAYER_NAME" \
  --query 'max_by(LayerVersions, &Version).LayerVersionArn' --output text 2>/dev/null || true)"
if [ -n "$LATEST_ARN" ] && [ "$LATEST_ARN" != "None" ]; then
  # REUSE GUARD — LAYER_NAME omits RUNTIME, so a runtime flip on the same noun would otherwise silently
  # hand back the other runtime's artifact. Refuse loudly instead of aliasing.
  LV="${LATEST_ARN##*:}"
  aws lambda get-layer-version --region "$AWS_REGION" --layer-name "$LAYER_NAME" --version-number "$LV" \
    --query 'CompatibleRuntimes' --output text | grep -q "$LAMBDA_RUNTIME" \
    || { echo "existing ${LAYER_NAME}:${LV} is not ${LAMBDA_RUNTIME}-compatible — RUNTIME changed under an existing name; pick a new LAYER noun or teardown first"; exit 1; }
  LAYER_VERSION_ARN="$LATEST_ARN"
  echo "reusing existing latest version: $LAYER_VERSION_ARN  (bump via Update)"
else
  LAYER_VERSION_ARN="$(aws lambda publish-layer-version --region "$AWS_REGION" --layer-name "$LAYER_NAME" \
    --description "$DESCRIPTION" \
    --compatible-runtimes $COMPAT_RUNTIMES --compatible-architectures x86_64 arm64 \
    --zip-file fileb://"$BUILD/layer.zip" --query LayerVersionArn --output text)"
  echo "published: $LAYER_VERSION_ARN"
fi
VERSION="${LAYER_VERSION_ARN##*:}"
```
```bash
# ✔ the version exists AND is well-formed — assert the VALUES, not just that the keys exist
SIZE="$(aws lambda get-layer-version --region "$AWS_REGION" --layer-name "$LAYER_NAME" --version-number "$VERSION" \
  --query 'Content.CodeSize' --output text)"
[ "$SIZE" -gt 0 ] && echo "CodeSize=${SIZE} (>0)" || { echo "layer version is EMPTY"; exit 1; }
aws lambda get-layer-version --region "$AWS_REGION" --layer-name "$LAYER_NAME" --version-number "$VERSION" \
  --query 'CompatibleRuntimes' --output text | grep -q "$LAMBDA_RUNTIME" \
  && echo "runtime ${LAMBDA_RUNTIME} compatible" || { echo "wrong runtime family"; exit 1; }
# ✔ (contract 3, at dogfood) version stability: note VERSION, re-run this whole §2 block, assert VERSION unchanged —
STABLE_V="$VERSION"   # re-apply must reuse, never mint: [ "$VERSION" = "$STABLE_V" ]
```
> → Live State: `LAYER_VERSION_ARN`, `VERSION`. **Provides** `lambda-layer(${LAYER_NAME}, ${LAYER_VERSION_ARN})`.

## 3. Acceptance — a function imports the package ONLY because of the layer  ✔  *(self-contained harness)*

> Proves the seam: a probe function that vendors nothing fails to import `PKG` with no layer, and succeeds once
> the layer is attached. The negative runs first (a genuine absence), then attach flips it to true. The throwaway
> role + function are cleaned at the end so a re-run starts clean.

```bash
TEST_FN="${LAYER_NAME}-probe"; TEST_ROLE="${LAYER_NAME}-probe-exec"
mkdir -p "$BUILD"
cat > "$BUILD/trust.json" <<'JSON'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}
JSON
# throwaway exec role (IAM → --no-session under a broker)
TEST_ROLE_ARN="$(aws iam get-role --role-name "$TEST_ROLE" --query Role.Arn --output text 2>/dev/null)" \
  || TEST_ROLE_ARN="$(aws iam create-role --role-name "$TEST_ROLE" \
       --assume-role-policy-document file://"$BUILD/trust.json" --query Role.Arn --output text)"
aws iam attach-role-policy --role-name "$TEST_ROLE" \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# probe handler — imports PKG; nothing vendored in THIS zip
if [ "$RUNTIME" = "python" ]; then
  cat > "$BUILD/probe.py" <<PY
def handler(event, context):
    try:
        m = __import__("${PKG}"); return {"imported": True, "path": getattr(m, "__file__", "")}
    except Exception as e:
        return {"imported": False, "error": str(e)}
PY
  ( cd "$BUILD" && zip -q probe.zip probe.py ); PROBE_HANDLER="probe.handler"
else
  # CommonJS, deliberately — Lambda exposes nodejs layers via NODE_PATH, and Node's ES-module resolver
  # IGNORES NODE_PATH (an .mjs probe reports imported:false even with a correct layer attached — the
  # classic layers-with-ESM gotcha). require() sees the layer.
  cat > "$BUILD/probe.cjs" <<JS
exports.handler = async () => {
  try { require("${PKG}"); return { imported: true }; }
  catch (e) { return { imported: false, error: String(e) }; }
};
JS
  ( cd "$BUILD" && zip -q probe.zip probe.cjs ); PROBE_HANDLER="probe.handler"
fi

# probe WITHOUT any layer. On a fresh run: create (IAM propagation retry — proven idiom from service.aws.md).
# On RE-ENTRY after a mid-crash (crashed between attach and cleanup): the reused probe may still carry the
# layer and stale code — converge it back to the no-layer baseline FIRST, or the negative below fails with a
# misleading "PKG resolved without a layer?!" (observe-before-act applies to the harness too).
if aws lambda get-function --region "$AWS_REGION" --function-name "$TEST_FN" >/dev/null 2>&1; then
  aws lambda update-function-code --region "$AWS_REGION" --function-name "$TEST_FN" \
    --zip-file fileb://"$BUILD/probe.zip" >/dev/null
  aws lambda wait function-updated --region "$AWS_REGION" --function-name "$TEST_FN"
  aws lambda update-function-configuration --region "$AWS_REGION" --function-name "$TEST_FN" \
    --layers >/dev/null            # bare --layers = empty list ⇒ detach all (baseline restored)
  aws lambda wait function-updated --region "$AWS_REGION" --function-name "$TEST_FN"
else
  for i in 1 2 3 4 5; do
    aws lambda create-function --region "$AWS_REGION" --function-name "$TEST_FN" \
      --runtime "$LAMBDA_RUNTIME" --role "$TEST_ROLE_ARN" --handler "$PROBE_HANDLER" \
      --zip-file fileb://"$BUILD/probe.zip" --timeout 10 >/dev/null 2>&1 && break
    [ "$i" = 5 ] && { echo "probe create failed"; exit 1; }; echo "role propagating — retry ${i}/5"; sleep 5
  done
fi
aws lambda wait function-active --region "$AWS_REGION" --function-name "$TEST_FN"
```
```bash
# NEGATIVE first — no layer ⇒ the import is genuinely absent
aws lambda invoke --region "$AWS_REGION" --function-name "$TEST_FN" \
  --cli-binary-format raw-in-base64-out --payload '{}' "$BUILD/neg.json" >/dev/null
grep -q '"imported": *false' "$BUILD/neg.json" \
  && echo "negative: without layer → import fails (as expected)" \
  || { echo "NEGATIVE FAILED — PKG resolved without a layer?!"; cat "$BUILD/neg.json"; exit 1; }

# attach the layer, then POSITIVE — same code now imports PKG from the layer
aws lambda update-function-configuration --region "$AWS_REGION" --function-name "$TEST_FN" \
  --layers "$LAYER_VERSION_ARN" >/dev/null
aws lambda wait function-updated --region "$AWS_REGION" --function-name "$TEST_FN"
aws lambda invoke --region "$AWS_REGION" --function-name "$TEST_FN" \
  --cli-binary-format raw-in-base64-out --payload '{}' "$BUILD/pos.json" >/dev/null
grep -q '"imported": *true' "$BUILD/pos.json" \
  && echo "positive: with layer → import OK" \
  || { echo "POSITIVE FAILED — layer not on the import path"; cat "$BUILD/pos.json"; exit 1; }
```
```bash
# clean the throwaway harness (the layer is the deliverable, not the probe)
aws lambda delete-function --region "$AWS_REGION" --function-name "$TEST_FN" 2>/dev/null || true
aws iam detach-role-policy --role-name "$TEST_ROLE" \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2>/dev/null || true
aws iam delete-role --role-name "$TEST_ROLE" 2>/dev/null || true
echo "probe harness removed"
```
> → Live State: fill verify rows; `status: live` (the layer stands; the probe was ephemeral).

## Update (idempotent reconcile / deliberate bump)  🟡

- **Bump the dependency** (new `PKG` version, changed shared code) → re-run §1, then force a **new** layer version
  by calling `publish-layer-version` directly (skip §2's reuse branch). Layers are append-only: the new version
  gets `:<n+1>`; consumers keep using the ARN they pinned until they re-discover the latest. (Proven live
  2026-07-05: bump minted `:2`, `max_by` re-discovery picked it up immediately.)
- **Widen/narrow runtimes or architectures** → these are set at publish time only; a change means a new version
  (re-publish). There is no in-place edit of an existing version.
- **Consumers pick up a bump** by re-discovering: `aws lambda list-layer-versions --layer-name <name>
  --query 'max_by(LayerVersions, &Version).LayerVersionArn'` → re-attach (`update-function-configuration
  --layers`). (`max_by`, not `[0]` — the API documents no ordering.)

## Teardown — observe-first, resumable  💥  *(delete every version; name-based ownership)*

> 💥 Human go. Layer versions are immutable and accumulate — teardown **loops** over all of them. ⚠ Ownership is
> **name-only** (layers are untaggable — no `ManagedBy` to verify), so this deletes purely by the deterministic
> `LAYER_NAME`; be sure the name is yours. Deleting a version a live function still references does not break that
> function (it keeps the already-resolved code) but the version can no longer be attached to new functions. Also
> removes the acceptance harness if a crashed §3 left it behind.

```bash
# stragglers from a failed §3 (usually already cleaned)
aws lambda delete-function --region "$AWS_REGION" --function-name "${LAYER_NAME}-probe" 2>/dev/null || true
aws iam detach-role-policy --role-name "${LAYER_NAME}-probe-exec" \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 2>/dev/null || true
aws iam delete-role --role-name "${LAYER_NAME}-probe-exec" 2>/dev/null || true

# 💥 every version of the layer, newest-first
for v in $(aws lambda list-layer-versions --region "$AWS_REGION" --layer-name "$LAYER_NAME" \
             --query 'LayerVersions[].Version' --output text 2>/dev/null); do
  aws lambda delete-layer-version --region "$AWS_REGION" --layer-name "$LAYER_NAME" --version-number "$v"
  echo "deleted ${LAYER_NAME}:${v}"
done
```
```bash
# ✔ no versions remain (list the versions themselves — page-safe, unlike length() which can emit one
#   number per page under auto-pagination)
LEFT="$(aws lambda list-layer-versions --region "$AWS_REGION" --layer-name "$LAYER_NAME" \
  --query 'LayerVersions[].Version' --output text 2>/dev/null || true)"
{ [ -z "$LEFT" ] || [ "$LEFT" = "None" ]; } && echo "layer gone" || { echo "versions remain: $LEFT"; exit 1; }
```
> → Live State: `status: gone`, clear realized ids.

## Composition — how this plugs into the fleet

`lambda-layer(${LAYER_NAME}, ARN)` is a producer with no runtime of its own — it exists to be **attached**.
[`service.aws.md`](./service.aws.md) consumes it via its **`BIND_LAYER`** knob (discovers the latest version by
name, attaches it with `--layers`, and its acceptance imports a module the layer supplies);
[`task-runner.aws.md`](./task-runner.aws.md)'s Lambdas can attach it the same way. The seam is a **plain ARN
string**, discovered from the cloud — no shared state file. There is no Cloudflare sibling: Workers resolve
dependencies at build/bundle time, so "shared layer" isn't a runtime primitive there — a genuine provider
asymmetry, not a missing binding.

## Deliberately not included

- **Cross-account / public sharing** (`add-layer-version-permission`) — a layer can be granted to other accounts
  or made public; that's a distribution concern with its own blast radius (a public layer is forever). Name the
  need first.
- **Compiled / native dependencies** — numpy, pillow, cryptography, headless-chromium: §1's manylinux pin now
  handles any dep that *ships* a manylinux wheel (that's most of them), but arch still matters (narrow
  `--compatible-architectures` to the consuming function) and a fat layer can blow the 250 MB unzipped
  ceiling. Source-only deps (no wheel) need a linux build host — out of this plan's scope.
- **Container-image packaging** — for very large dependency sets, a container image (up to 10 GB) beats layers;
  that's a different function-packaging shape entirely, out of this plan's scope.
- **Content-hash auto-republish** — detecting "the dependency changed" to auto-mint a version is deliberately
  NOT automated (a pip install's timestamps make the zip hash unstable run-to-run); bumping a version is an
  explicit `update`, so version history stays a record of intent, not of incidental rebuilds.
