| 1 | --- |
| 2 | title: Resource Lifecycle |
| 3 | description: How alchemy plans, applies, replaces, and destroys resources — and how to think about idempotency and recovery. |
| 4 | --- |
| 5 | |
| 6 | import Terminal from "../../../components/Terminal.astro"; |
| 7 | import DAG from "../../../components/DAG.astro"; |
| 8 | |
| 9 | Every [Resource](/infrastructure-as-code/resource) goes through the same lifecycle: |
| 10 | **plan → reconcile → (replace) → delete**. The plan classifies each |
| 11 | resource as create, update, replace, delete, or no-op, but the |
| 12 | provider implements a single `reconcile` function that converges the |
| 13 | cloud's actual state to what's declared — whether that's the first |
| 14 | provisioning, a routine update, or an adoption takeover. For the CLI |
| 15 | flags that drive these operations, see the |
| 16 | [CLI reference](/cli). |
| 17 | |
| 18 | ## The lifecycle, end-to-end |
| 19 | |
| 20 | <DAG |
| 21 | nodes={[ |
| 22 | { id: "Plan", tone: "accent" }, |
| 23 | { id: "Reconcile (create)" }, |
| 24 | { id: "Reconcile (update)" }, |
| 25 | { id: "Replace", tone: "warn" }, |
| 26 | { id: "Delete", tone: "danger" }, |
| 27 | { id: "Noop" }, |
| 28 | ]} |
| 29 | edges={[ |
| 30 | { from: "Plan", to: "Reconcile (create)", label: "new" }, |
| 31 | { from: "Plan", to: "Reconcile (update)", label: "changed" }, |
| 32 | { from: "Plan", to: "Replace", label: "breaking" }, |
| 33 | { from: "Plan", to: "Delete", label: "removed" }, |
| 34 | { from: "Plan", to: "Noop", label: "unchanged" }, |
| 35 | { from: "Replace", to: "Delete", dashed: true }, |
| 36 | ]} |
| 37 | /> |
| 38 | |
| 39 | Both create and update intents resolve to the provider's single |
| 40 | `reconcile` function. Replace runs `reconcile` against a fresh |
| 41 | instance id and then deletes the old generation. The same engine |
| 42 | drives `alchemy deploy`, `alchemy destroy`, and `alchemy dev`. |
| 43 | |
| 44 | ## Plan |
| 45 | |
| 46 | When you run `alchemy deploy`, alchemy first **plans** the change. |
| 47 | It compares the desired state (what your code declares) against the |
| 48 | last persisted state and classifies each resource: |
| 49 | |
| 50 | - **Create** (`+`) — declared in code, not in state |
| 51 | - **Update** (`~`) — declared and persisted, but properties differ |
| 52 | - **Replace** (`±`) — change requires destroy-and-recreate |
| 53 | - **Delete** (`-`) — persisted but no longer declared |
| 54 | - **No-op** (`•`) — unchanged |
| 55 | |
| 56 | <Terminal content={`[u]Plan[/u]: [g]1 to create[/g], [y]1 to update[/y] |
| 57 | |
| 58 | [g]+[/g] [b]Queue[/b] [d](AWS.SQS.Queue)[/d] |
| 59 | [y]~[/y] [b]Worker[/b] [d](Cloudflare.Worker)[/d] |
| 60 | [d]•[/d] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d]`} /> |
| 61 | |
| 62 | The classification comes from each provider's `diff` function. See |
| 63 | [Provider › diff](/infrastructure-as-code/provider#diff) for how providers decide |
| 64 | between in-place updates and replacements. |
| 65 | |
| 66 | Use `alchemy plan` (or `alchemy deploy --dry-run`) to see the plan |
| 67 | without applying it. |
| 68 | |
| 69 | ## Reconcile |
| 70 | |
| 71 | Whether a resource is being created for the first time, updated in |
| 72 | place, or adopted from existing infrastructure, alchemy calls a |
| 73 | single function: `provider.reconcile`. |
| 74 | |
| 75 | Reconcile must be **convergent**: given the desired props, it |
| 76 | observes live cloud state and brings it to that state regardless of |
| 77 | starting point. It must also be safe to retry — a partial reconcile |
| 78 | that crashed midway resumes correctly on the next run, because |
| 79 | physical names are deterministic from `stack/stage/logical-id` and |
| 80 | the reconciler finds the existing resource instead of duplicating |
| 81 | it. The parameter contract (`news`/`output`/`olds`/`bindings`) and |
| 82 | the observe → ensure → sync shape are the provider's concern — see |
| 83 | [Provider › reconcile](/infrastructure-as-code/provider#reconcile). |
| 84 | |
| 85 | A second pass — **convergence** — re-runs `reconcile` for any |
| 86 | resource whose inputs changed because an upstream output changed |
| 87 | mid-deploy. |
| 88 | |
| 89 | ## Replace |
| 90 | |
| 91 | Some property changes can't be applied in place — for example, |
| 92 | changing a DynamoDB table's partition key. The provider's `diff` |
| 93 | returns `{ action: "replace" }`, and alchemy: |
| 94 | |
| 95 | 1. Creates a new resource with a new instance ID |
| 96 | 2. Updates downstream resources to reference the new resource |
| 97 | 3. Deletes the old resource |
| 98 | |
| 99 | Because new and old coexist briefly, dependents get a clean cutover |
| 100 | without downtime. |
| 101 | |
| 102 | Changing a **logical ID** also plans a replacement — if you merely |
| 103 | renamed the resource, declare it with `renamedFrom` instead; see |
| 104 | [Renaming Resources](/infrastructure-as-code/renaming). |
| 105 | |
| 106 | ## Delete |
| 107 | |
| 108 | `provider.delete` is called when a resource disappears from your |
| 109 | code, when a replacement supersedes it, or when you run |
| 110 | `alchemy destroy`. Like create, delete must be **idempotent**: |
| 111 | deleting an already-gone resource is a success, not an error. |
| 112 | |
| 113 | `alchemy destroy` is just a plan where every persisted resource is |
| 114 | marked for deletion. Resources are removed in **reverse dependency |
| 115 | order** — dependents go first. |
| 116 | |
| 117 | <Terminal content={`[u]Plan[/u]: [r]2 to delete[/r] |
| 118 | |
| 119 | [r]-[/r] [b]Worker[/b] [d](Cloudflare.Worker)[/d] |
| 120 | [r]-[/r] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d] |
| 121 | |
| 122 | Proceed? |
| 123 | ◉ Yes ○ No |
| 124 | [r]✗[/r] [b]Worker[/b] [d](Cloudflare.Worker)[/d] [r]deleted[/r] |
| 125 | [r]✗[/r] [b]Bucket[/b] [d](Cloudflare.R2.Bucket)[/d] [r]deleted[/r]`} /> |
| 126 | |
| 127 | ## Removal policy |
| 128 | |
| 129 | Each resource carries a **removal policy** that decides what happens |
| 130 | to the physical cloud object when the resource is deleted — orphaned, |
| 131 | destroyed, or superseded as a replacement's old generation: |
| 132 | |
| 133 | | Policy | What the engine does | |
| 134 | | ----------- | -------------------------------------------------------- | |
| 135 | | `destroy` | Calls `provider.delete`, then drops the state row | |
| 136 | | `retain` | Skips `provider.delete`, then drops the state row | |
| 137 | |
| 138 | Under `retain`, alchemy forgets the resource either way — only the |
| 139 | cloud object survives. Most resource types default to `destroy`; a few |
| 140 | whose contents are irreplaceable default to `retain` (e.g. |
| 141 | [`GitHub.Repository`](/github/repository), `Cloudflare.Zone`). |
| 142 | |
| 143 | Set the policy by piping a declaration through `retain()` or |
| 144 | `destroy()`. It applies to every resource declared inside the piped |
| 145 | effect, so it can decorate one resource or a whole scope: |
| 146 | |
| 147 | ```typescript |
| 148 | import * as RemovalPolicy from "alchemy/RemovalPolicy"; |
| 149 | |
| 150 | Effect.gen(function* () { |
| 151 | const stack = yield* Stack; |
| 152 | |
| 153 | // never deleted by alchemy |
| 154 | const uploads = yield* R2.Bucket("Uploads").pipe(RemovalPolicy.retain()); |
| 155 | |
| 156 | // retained in prod, torn down in every other stage |
| 157 | const cache = yield* R2.Bucket("Cache").pipe( |
| 158 | RemovalPolicy.retain(stack.stage === "prod"), |
| 159 | ); |
| 160 | }); |
| 161 | ``` |
| 162 | |
| 163 | The policy is a decoration, not a prop, so changing it produces no |
| 164 | diff — the resource plans as a **no-op** and the plan reports no |
| 165 | changes. The deploy still persists the new policy onto the state row |
| 166 | (the orphan delete reads it from there, long after the declaration is |
| 167 | gone), so a policy change takes effect from the very next deploy. |
| 168 | |
| 169 | :::caution |
| 170 | `retain` protects the resource from *alchemy*, not from the provider's |
| 171 | own delete semantics. Deleting a resource still cascades however that |
| 172 | cloud API cascades — e.g. deleting a bucket that a provider empties |
| 173 | first destroys its objects. Retain a resource **before** the deploy |
| 174 | that would remove it, and prefer moving irreplaceable data behind a |
| 175 | retained resource. |
| 176 | ::: |
| 177 | |
| 178 | ## Idempotency and recovery |
| 179 | |
| 180 | State persistence can fail after the cloud operation succeeds — the |
| 181 | network drops between "bucket created" and "state saved". Alchemy |
| 182 | handles this by requiring `reconcile` and `delete` to be safe to |
| 183 | retry: |
| 184 | |
| 185 | - **Reconcile**: deterministic physical names plus the observe-step |
| 186 | mean a retry finds the existing resource instead of creating a |
| 187 | duplicate, and re-syncs any aspect that drifted. |
| 188 | - **Delete**: a missing resource is treated as already deleted. |
| 189 | - **Read**: providers can implement `read` so alchemy can recover |
| 190 | state from the live cloud when persistence fails partway, and to |
| 191 | detect adoptable resources on a fresh state store. |
| 192 | |
| 193 | ## Adoption |
| 194 | |
| 195 | When planning a resource that has no prior state, the engine calls |
| 196 | `provider.read` (if the provider implements it). This serves two |
| 197 | overlapping purposes: |
| 198 | |
| 199 | - **State recovery** — the resource was created on a previous |
| 200 | deploy, but state was lost between the cloud op succeeding and the |
| 201 | store persisting. `read` finds the live resource and the engine |
| 202 | rebuilds `created` state from its attributes. |
| 203 | - **Adoption** — you're deploying against existing infrastructure |
| 204 | you didn't manage with Alchemy yet (or you wiped state |
| 205 | intentionally). `read` recognizes the resource and the engine |
| 206 | imports it into the new state. |
| 207 | |
| 208 | A resource `read` recognizes as ours is adopted silently. One that |
| 209 | exists but *isn't* ours fails with `OwnedBySomeoneElse` — re-running |
| 210 | with `--adopt` (or scoping the effect with `adopt(true)`) unlocks |
| 211 | the takeover. How providers signal ownership through `read`'s |
| 212 | return value is part of the provider contract — see |
| 213 | [Provider › read](/infrastructure-as-code/provider#read). For the CLI flag, see |
| 214 | [Adopting Resources](/cli/adopting-resources). |
| 215 | |
| 216 | ## Errors |
| 217 | |
| 218 | - **Retryable errors** (eventual consistency, dependency races) are |
| 219 | retried automatically with backoff. |
| 220 | - **Non-retryable errors** (validation, authorization) fail |
| 221 | immediately and surface in the plan output. |
| 222 | - **Partial failures** are safe to re-run thanks to idempotency. |
| 223 | |
| 224 | ## Driving the lifecycle from the CLI |
| 225 | |
| 226 | The same engine powers all of these commands: |
| 227 | |
| 228 | | Command | What it does | |
| 229 | | ------------------ | --------------------------------------------- | |
| 230 | | `alchemy plan` | Run plan, print diff, exit | |
| 231 | | `alchemy deploy` | Plan, prompt for approval, apply | |
| 232 | | `alchemy destroy` | Plan with everything marked deleted, apply | |
| 233 | | `alchemy dev` | Plan + apply continuously on file changes | |
| 234 | |
| 235 | See the [CLI reference](/cli) for the full set of flags |
| 236 | (`--yes`, `--force`, `--dry-run`, `--stage`, `--profile`, ...). |
| 237 | |
| 238 | Every lifecycle operation on this page is implemented per resource |
| 239 | type by a [Provider](/infrastructure-as-code/provider). |
| 240 | |
| 241 | ## Where next |
| 242 | |
| 243 | - [Renaming Resources](/infrastructure-as-code/renaming) — migrate state |
| 244 | across a logical ID change instead of replacing. Next page. |
| 245 | - [Providers](/infrastructure-as-code/provider) — the object that implements |
| 246 | `reconcile`, `delete`, `diff`, and `read` for a resource type. |
| 247 | - [CLI](/cli) — the commands and flags that drive the |
| 248 | lifecycle. |
| 249 | - [State Store](/state-store) — where the persisted state |
| 250 | behind the plan lives. |