SST v4 for AWS
When to Use
Use this skill when you need sST v4 (Ion) expert for managing AWS resources as code with the Pulumi-backed framework. Use when writing or editing sst.config.ts, building infra/ modules (sst.aws.Function/Bucket/Dynamo/Cron/Service/Router, sst.Secret, sst.Linkable, raw aws.* Pulumi resources), wiring resource links,...
SST v4 (the "Ion" engine) is a Pulumi-backed IaC framework: you describe AWS
resources in TypeScript and SST/Pulumi reconciles them into your account. It
gives you high-level sst.aws.* components (Function, Bucket, Dynamo, Cron,
Service, …) that expand into many underlying resources, plus an escape hatch to
any raw Pulumi aws.* resource for the long tail. This skill encodes a
production-proven way to author, link, test, deploy, and troubleshoot SST
stacks on AWS — distilled from real multi-stack projects that have paid for
each lesson with a prod incident.
SST and Pulumi are third-party — verify current syntax with Context7
(resolve-library-id → query-docs for sst or pulumi-aws) when you're
unsure about a component's options. Verify AWS-side facts (service limits,
model IDs, IAM action names, region availability) with the AWS docs MCP, never
from memory. The patterns here are the how; the docs are the what.
When you're invoked
Figure out which mode you're in and jump to the right reference:
| Situation |
Go to |
| New project, or adding a resource/module to an existing SST app |
Author → references/authoring.md |
| Wiring one module's output into another (links, SSM, IAM scope) |
Author → references/authoring.md § Sharing |
| Writing tests for infra so changes don't silently break |
Test → references/testing.md |
| Running a deploy, or a deploy just failed |
Deploy/Operate → references/deploy-and-troubleshoot.md |
| Migrating a resource between Pulumi types, renaming a physical name |
Deploy/Operate → references/deploy-and-troubleshoot.md § Migrations |
Always read the relevant reference before editing — they carry the why behind
each rule, which matters more than the rule itself.
Orientation: read the repo before you touch it
SST projects are conventional but not identical. Before editing, build a quick
map so your change matches the house style instead of fighting it:
sst.config.ts — the app name, home, providers/region, defaultTags,
any global $transform (Node runtime pin, bundle fixups), and the order in
which run() imports infra/ modules. The import order is the dependency
order; respect it.
infra/ — one file per domain (storage, functions, api, observability…).
This is where resources are declared. Check for an infra/CLAUDE.md — these
projects keep IaC-specific rules there, and it's the single most valuable
file to read first.
infra/tests/ — source-level Vitest assertions that pin resource
invariants. If they exist, your change must keep them green and probably
needs a new assertion.
package.json / .nvmrc — package manager (npm vs pnpm), Node version,
and the sst/pulumi versions actually installed.
Run npx sst version to confirm you're on v4/Ion (the $config + .sst/platform/
signature). v2/v3 ("SST Classic", CDK-based) is a different framework — these
patterns don't apply there.
The conventions, and which are universal vs tunable
The projects this skill is built from share a deliberate house style. Some of it
is universal (true for any SST v4 + AWS project — apply it everywhere); some
is project-specific (a sensible default these projects chose — adopt it for
consistency, but recognize a project may differ).
Universal — these principles hold for any SST v4 + AWS project:
- Control the Node runtime deliberately, in one place. Don't leave it to
whatever the installed SST happens to default to. The idiom is a single global
$transform(sst.aws.Function, (args) => { args.runtime ??= "nodejs24.x" }) in
run() — ??= is correct here (the transform runs before the component
applies its own default, so it fills in only when the user didn't set one).
Recent SST already defaults to a current Node runtime, so check the installed
default first (Context7); the transform is then version-independence insurance
so a future SST downgrade can't silently move your fleet. See
references/authoring.md.
- Never interpolate a Pulumi
Output<T> into a plain JS template literal.
Use $interpolate (or pulumi.interpolate). A bare top-level
`${bucket.arn}/*` stringifies the Output to a [Output<T>] placeholder
and produces a broken ARN that only fails at deploy time (it type-checks and
sst dev runs fine). The fix is $interpolate`${bucket.arn}/*`. This
has caused prod deploy outages. See references/authoring.md § Outputs.
- Migrating a resource between Pulumi types should default to two PRs —
Pulumi creates-before-destroys, so for a uniqueness-constrained AWS name
(bucket, IAM role, gateway) the old resource still owns it and the create
fails with
ConflictException. Two sequential deploys (teardown, then
recreate) is the conservative default; aliases: / pulumi import / state
surgery can bridge identity in some cases but only with a reviewed plan. See
references/deploy-and-troubleshoot.md § Migrations.
- Prefer typed
sst.aws.* / aws.* resources over the
aws.cloudcontrol.Resource escape hatch. CloudControl outputs are
stringly-typed and oneOf fields don't patch cleanly. Use it only when no
typed resource exists yet, and migrate off it when one ships.
Project-specific defaults — adopt for consistency, but confirm per repo:
- Region
ap-northeast-1, home: "aws", and defaultTags carrying
Project / Stage / ManagedBy: "sst".
- Stage-gated lifecycle:
removal: stage === "prod" ? "retain" : "remove"
and protect: stage === "prod" so prod resources survive a stack tear-down
and non-prod previews clean up.
- SSM Parameter Store as the out-of-graph contract under a
/{app}/{stage}/{domain}/... prefix — for consumers that aren't in the
Pulumi graph (CI scripts, sibling apps, operators). For same-app Lambdas,
prefer SST link: (it wires a real dependency edge and grants IAM); don't
route same-app sharing through SSM. See references/authoring.md § Sharing.
- Lazy
await import("./infra/<module>") inside run() so sst dev
hot-reload stays light. (For testing, a module export still runs its top-level
new sst.aws.* unless it's wrapped in a factory function — see
references/testing.md for how to test infra.)
- Source-level Vitest tests on every infra module — a lightweight,
house-style regression net asserting on the source text (resource names,
index shapes, IAM scopes). It's a deliberate choice, not an SST limit: Pulumi
does support runtime mocks (
@pulumi/pulumi/runtime) for behavioral graph
tests when a module has real logic. Source assertions don't replace a
preview-deploy + smoke test. See references/testing.md.
- An observability gate: every new Lambda/queue/schedule gets an alarm and
structured logging before merge. Whether you enforce this depends on the
project, but it's cheap insurance. See
references/deploy-and-troubleshoot.md
§ Observability.
When you introduce a convention, say which bucket it's in ("this is universal"
vs "matching this repo's house style") so the user can override the
project-specific ones deliberately.
Working rhythm
- Orient (above) — map config, modules, tests, tooling.
- Verify syntax with Context7 / AWS docs MCP if anything is non-obvious.
Don't guess at a component's option name.
- Author the resource/module following
references/authoring.md. Match the
surrounding file's commenting density and naming — these projects comment the
why heavily, and a terse one-liner in a heavily-annotated file reads as a
regression.
- Test — add or update source-level assertions (
references/testing.md) and
run npx vitest (or the repo's test script). Run npx sst diff and/or
tsc --noEmit to catch type and plan errors before deploying.
- Deploy/operate per
references/deploy-and-troubleshoot.md. Confirm the
target account with aws sts get-caller-identity before any sst deploy.
- Clean up any exported state files — they contain account IDs and ARNs and
must not linger in
/tmp or chat history.
What good looks like
- The change is the smallest diff that satisfies the requirement, in the right
infra/ module, wired into run() in dependency order.
- Every Lambda gets the right runtime via the global transform (you didn't
hand-set
runtime unless intentionally diverging — e.g. a Python function).
- Cross-resource references use
link: (in-graph) and/or $interpolate-scoped
IAM; outputs other tools consume are published to SSM under the stage prefix.
- New infra has a matching source-level test, and the existing suite stays green.
- You confirmed AWS-side facts via the docs MCP and SST/Pulumi syntax via
Context7 rather than relying on recall.
- Anything irreversible (deploy,
sst remove, a resource-type migration) was
flagged to the user with the account it targets, and migrations were planned
as two PRs, not one.
Limitations
- Use this skill only when the task clearly matches its upstream source and local project context.
- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.
- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.
Source: sickn33/agentic-awesome-skills → skills/aws-sst-development/SKILL.md
Also appears in: sickn33/agentic-awesome-skills/plugins/agentic-awesome-skills/skills/aws-sst-development/SKILL.md, sickn33/agentic-awesome-skills/plugins/agentic-awesome-skills-claude/skills/aws-sst-development/SKILL.md
1---2name: aws-sst-development3description: SST v4 (Ion) expert for managing AWS resources as code with the Pulumi-backed framework. Use when writing or editing sst.config.ts, building infra/ modules (sst.aws.Function/Bucket/Dynamo/Cron/Service/Router, sst.Secret, sst.Linkable, raw aws.* Pulumi resources), wiring resource links,...4---567# SST v4 for AWS8## When to Use910Use this skill when you need sST v4 (Ion) expert for managing AWS resources as code with the Pulumi-backed framework. Use when writing or editing sst.config.ts, building infra/ modules (sst.aws.Function/Bucket/Dynamo/Cron/Service/Router, sst.Secret, sst.Linkable, raw aws.* Pulumi resources), wiring resource links,...111213SST v4 (the "Ion" engine) is a Pulumi-backed IaC framework: you describe AWS14resources in TypeScript and SST/Pulumi reconciles them into your account. It15gives you high-level `sst.aws.*` components (Function, Bucket, Dynamo, Cron,16Service, …) that expand into many underlying resources, plus an escape hatch to17*any* raw Pulumi `aws.*` resource for the long tail. This skill encodes a18production-proven way to author, link, test, deploy, and troubleshoot SST19stacks on AWS — distilled from real multi-stack projects that have paid for20each lesson with a prod incident.2122**SST and Pulumi are third-party — verify current syntax with Context7**23(`resolve-library-id` → `query-docs` for `sst` or `pulumi-aws`) when you're24unsure about a component's options. Verify AWS-side facts (service limits,25model IDs, IAM action names, region availability) with the AWS docs MCP, never26from memory. The patterns here are the *how*; the docs are the *what*.2728## When you're invoked2930Figure out which mode you're in and jump to the right reference:3132| Situation | Go to |33|-----------|-------|34| New project, or adding a resource/module to an existing SST app | **Author** → `references/authoring.md` |35| Wiring one module's output into another (links, SSM, IAM scope) | **Author** → `references/authoring.md` § Sharing |36| Writing tests for infra so changes don't silently break | **Test** → `references/testing.md` |37| Running a deploy, or a deploy just failed | **Deploy/Operate** → `references/deploy-and-troubleshoot.md` |38| Migrating a resource between Pulumi types, renaming a physical name | **Deploy/Operate** → `references/deploy-and-troubleshoot.md` § Migrations |3940Always read the relevant reference before editing — they carry the *why* behind41each rule, which matters more than the rule itself.4243## Orientation: read the repo before you touch it4445SST projects are conventional but not identical. Before editing, build a quick46map so your change matches the house style instead of fighting it:47481. **`sst.config.ts`** — the app name, `home`, providers/region, `defaultTags`,49 any global `$transform` (Node runtime pin, bundle fixups), and the order in50 which `run()` imports `infra/` modules. The import order *is* the dependency51 order; respect it.522. **`infra/`** — one file per domain (storage, functions, api, observability…).53 This is where resources are declared. Check for an `infra/CLAUDE.md` — these54 projects keep IaC-specific rules there, and it's the single most valuable55 file to read first.563. **`infra/tests/`** — source-level Vitest assertions that pin resource57 invariants. If they exist, your change must keep them green and probably58 needs a new assertion.594. **`package.json` / `.nvmrc`** — package manager (npm vs pnpm), Node version,60 and the `sst`/`pulumi` versions actually installed.6162Run `npx sst version` to confirm you're on v4/Ion (the `$config` + `.sst/platform/`63signature). v2/v3 ("SST Classic", CDK-based) is a different framework — these64patterns don't apply there.6566## The conventions, and which are universal vs tunable6768The projects this skill is built from share a deliberate house style. Some of it69is **universal** (true for any SST v4 + AWS project — apply it everywhere); some70is **project-specific** (a sensible default these projects chose — adopt it for71consistency, but recognize a project may differ).7273**Universal — these principles hold for any SST v4 + AWS project:**7475- **Control the Node runtime deliberately, in one place.** Don't leave it to76 whatever the installed SST happens to default to. The idiom is a single global77 `$transform(sst.aws.Function, (args) => { args.runtime ??= "nodejs24.x" })` in78 `run()` — `??=` is correct here (the transform runs before the component79 applies its own default, so it fills in only when the user didn't set one).80 Recent SST already defaults to a current Node runtime, so check the installed81 default first (Context7); the transform is then version-independence insurance82 so a future SST downgrade can't silently move your fleet. See83 `references/authoring.md`.84- **Never interpolate a Pulumi `Output<T>` into a plain JS template literal.**85 Use `$interpolate` (or `pulumi.interpolate`). A bare top-level86 `` `${bucket.arn}/*` `` stringifies the `Output` to a `[Output<T>]` placeholder87 and produces a broken ARN that only fails at deploy time (it type-checks and88 `sst dev` runs fine). The fix is `$interpolate``` `${bucket.arn}/*` ``. This89 has caused prod deploy outages. See `references/authoring.md` § Outputs.90- **Migrating a resource between Pulumi *types* should default to two PRs** —91 Pulumi creates-before-destroys, so for a uniqueness-constrained AWS name92 (bucket, IAM role, gateway) the old resource still owns it and the create93 fails with `ConflictException`. Two sequential deploys (teardown, then94 recreate) is the conservative default; `aliases:` / `pulumi import` / state95 surgery can bridge identity in some cases but only with a reviewed plan. See96 `references/deploy-and-troubleshoot.md` § Migrations.97- **Prefer typed `sst.aws.*` / `aws.*` resources over the98 `aws.cloudcontrol.Resource` escape hatch.** CloudControl outputs are99 stringly-typed and `oneOf` fields don't patch cleanly. Use it only when no100 typed resource exists yet, and migrate off it when one ships.101102**Project-specific defaults — adopt for consistency, but confirm per repo:**103104- **Region `ap-northeast-1`**, `home: "aws"`, and `defaultTags` carrying105 `Project` / `Stage` / `ManagedBy: "sst"`.106- **Stage-gated lifecycle**: `removal: stage === "prod" ? "retain" : "remove"`107 and `protect: stage === "prod"` so prod resources survive a stack tear-down108 and non-prod previews clean up.109- **SSM Parameter Store as the out-of-graph contract** under a110 `/{app}/{stage}/{domain}/...` prefix — for consumers that aren't in the111 Pulumi graph (CI scripts, sibling apps, operators). For *same-app* Lambdas,112 prefer SST `link:` (it wires a real dependency edge and grants IAM); don't113 route same-app sharing through SSM. See `references/authoring.md` § Sharing.114- **Lazy `await import("./infra/<module>")` inside `run()`** so `sst dev`115 hot-reload stays light. (For testing, a module export still runs its top-level116 `new sst.aws.*` unless it's wrapped in a factory function — see117 `references/testing.md` for how to test infra.)118- **Source-level Vitest tests** on every infra module — a lightweight,119 house-style regression net asserting on the *source text* (resource names,120 index shapes, IAM scopes). It's a deliberate choice, not an SST limit: Pulumi121 *does* support runtime mocks (`@pulumi/pulumi/runtime`) for behavioral graph122 tests when a module has real logic. Source assertions don't replace a123 preview-deploy + smoke test. See `references/testing.md`.124- **An observability gate**: every new Lambda/queue/schedule gets an alarm and125 structured logging before merge. Whether you enforce this depends on the126 project, but it's cheap insurance. See `references/deploy-and-troubleshoot.md`127 § Observability.128129When you introduce a convention, say which bucket it's in ("this is universal"130vs "matching this repo's house style") so the user can override the131project-specific ones deliberately.132133## Working rhythm1341351. **Orient** (above) — map config, modules, tests, tooling.1362. **Verify syntax** with Context7 / AWS docs MCP if anything is non-obvious.137 Don't guess at a component's option name.1383. **Author** the resource/module following `references/authoring.md`. Match the139 surrounding file's commenting density and naming — these projects comment the140 *why* heavily, and a terse one-liner in a heavily-annotated file reads as a141 regression.1424. **Test** — add or update source-level assertions (`references/testing.md`) and143 run `npx vitest` (or the repo's `test` script). Run `npx sst diff` and/or144 `tsc --noEmit` to catch type and plan errors before deploying.1455. **Deploy/operate** per `references/deploy-and-troubleshoot.md`. Confirm the146 target account with `aws sts get-caller-identity` before any `sst deploy`.1476. **Clean up** any exported state files — they contain account IDs and ARNs and148 must not linger in `/tmp` or chat history.149150## What good looks like151152- The change is the smallest diff that satisfies the requirement, in the right153 `infra/` module, wired into `run()` in dependency order.154- Every Lambda gets the right runtime via the global transform (you didn't155 hand-set `runtime` unless intentionally diverging — e.g. a Python function).156- Cross-resource references use `link:` (in-graph) and/or `$interpolate`-scoped157 IAM; outputs other tools consume are published to SSM under the stage prefix.158- New infra has a matching source-level test, and the existing suite stays green.159- You confirmed AWS-side facts via the docs MCP and SST/Pulumi syntax via160 Context7 rather than relying on recall.161- Anything irreversible (deploy, `sst remove`, a resource-type migration) was162 flagged to the user with the account it targets, and migrations were planned163 as two PRs, not one.164165## Limitations166167- Use this skill only when the task clearly matches its upstream source and local project context.168- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.169- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.170171---172173**Source:** [`sickn33/agentic-awesome-skills`](https://github.com/sickn33/agentic-awesome-skills) → `skills/aws-sst-development/SKILL.md`174175**Also appears in:** `sickn33/agentic-awesome-skills/plugins/agentic-awesome-skills/skills/aws-sst-development/SKILL.md`, `sickn33/agentic-awesome-skills/plugins/agentic-awesome-skills-claude/skills/aws-sst-development/SKILL.md`