Skip to content
郭立 (leeguoo)

You Can’t Import a GSI by Itself

A prod DynamoDB table had its billing and indexes changed directly, bypassing CloudFormation, and the whole stack ended up stuck in rollback for five days. A write-up of what caused the drift and how it was fixed, plus one counterintuitive point: CFN import works at the whole-resource level.

The incident started like this: someone thought the provisioned capacity on the prod DynamoDB table wasn’t enough, so they directly ran two aws dynamodb update-table commands — switched billing from PROVISIONED to on-demand, and added a GSI while they were at it. They tested it, it worked, done.

They did not go through CloudFormation.

A week later, during a completely unrelated release, CloudFormation got stuck hard in rollback, the whole stack entered UPDATE_ROLLBACK_COMPLETE, and nobody dared click deploy again. The error message was just one line: Attempting to create an index which already exists.

CFN Only Trusts Its Own Ledger, Not Reality

CloudFormation is essentially a ledger: you give it a template, it records “what this set of resources should look like,” then creates and updates according to that. The problem is that when it computes a diff, it compares the new template vs its own ledger — it never checks what the cloud resource actually looks like.

So when someone bypasses it and changes a resource directly, the ledger no longer matches reality. This is drift.

CFN ledger (what it thinks) Billing: PROVISIONED GSI: 4 ≠ Cloud reality (changed out of band) Billing: PAY_PER_REQUEST GSI: 5 You submit a new “correct” template (5 GSIs) — CFN compares against its stale ledger (4 GSIs): “one is missing, I’ll create it” → That GSI already exists in reality → already exists → rollback 💥
The fatal part of drift: CFN calculates differences from its ledger and tries to create a resource that already exists in reality

The nastier part is rollback itself: when CFN rolls back a failed “add GSI” operation, it will try to delete the GSI it thinks it just created — but that is actually the index your production traffic is already using. Rollback deletes the wrong thing. That is why nobody dared touch it: every deployment was a gamble.

The Counterintuitive Part: You Can’t Import a GSI

The natural thought is: then just let CFN “claim” the out-of-band GSI again, right? CloudFormation has resource import.

But there is a trap here: import works at the whole-resource level. You can import an entire table or an entire bucket, but a GSI is not an independent resource; it is a property of AWS::DynamoDB::Table. You cannot import “one child property inside a table that is already managed by the stack” on its own.

To make CFN accept drift in a child property, you can only remove the whole table from the stack first, then import the whole table back in.

The Fix: Remove → Retain → Reimport

The key is to use DeletionPolicy: Retain — when CFN “lets go of” a resource, it does not actually delete it.

1. Add Retain Delete it = keep real table 2. Remove from template Detach CFN ledger 3. Match reality 5 GSIs / on-demand 4. IMPORT Claim again Ledger = reality →→→→ The real table serves traffic the whole time: step 2’s detach calls no DynamoDB API and only changes the CFN ledger
Remove the whole table, then import it back; the real table stays online the whole time

When writing the import changeset in step 4, there are also two traps that are not placed anywhere obvious. You only learn them after stepping on them once:

aws cloudformation create-change-set \
  --change-set-type IMPORT \
  --template-body file://original.yaml \   # Must be Original YAML (before transform), not processed JSON
  --parameters "$PARAMS" \                 # Pass all stack parameters with UsePreviousValue
  --resources-to-import '[{"ResourceType":"AWS::DynamoDB::Table",...}]'
  • You must pass UsePreviousValue for all parameters at once (not just the required ones). Any omitted parameter falls back to the template default, causing CFN to decide that a bunch of unrelated resources “changed” and reject the import with “N resources modified.”
  • The template must be the Original YAML (before transform), not the processed JSON returned by get-template by default. If you submit the processed version back, CFN will still detect differences in resources expanded and generated by SAM.

A Few Notes for Myself

The really scary part was not this procedure — the detach step does not touch DynamoDB at all, the table keeps serving traffic, and you can roll back if you mess it up. The scary part was that it stayed buried for a week before exploding, and it exploded during a release that had absolutely nothing to do with it.

  • Bypassing IaC and directly running update-table / changing things in the console is a slow poison. It works for the moment, but the cost is written into the ledger, waiting to be settled all at once when the next unlucky person ships a release.
  • CFN’s diff is “template vs. ledger”; it never includes “reality.” So detect-stack-drift is worth running regularly — don’t wait for it to tell you during a rollback.
  • import works at resource granularity. For drift in child attributes (GSI, one item in a parameter group…), accept your fate and reimport the whole resource.

That table is now back under CFN, the ledger matches reality, and detect-stack-drift is clean. For the next release, we finally don’t have to gamble.

More in this category · Tinkering & Building

Jun 18, 2026
1 min read
$5 a Month, Withstanding 860K RPS and Still Not Topped Out — Architecture Notes and an Honest Postmortem for a Hotspot KV
A second-level hotspot KV running on Cloudflare Workers (starting at $5/month): multi-region load testing pushed the read path past 860K RPS with zero worker errors, while the load generators hit their bottleneck first. This article explains its design, the full investigation of two real incidents (the first time we thought we had fixed it; the second time we found the real root cause: reads and writes were squeezed into the same queue of the same single-threaded object), and the final solution: separate reads from writes, plus redefining what “strong consistency” actually means.
→
Jun 27, 2026
5 min read
Synology DSM SSH Port Still Won’t Open After Enabling It: A Port Drift Retrospective
After enabling SSH or changing its port in the Synology DSM panel, the port may still fail to open. The issue is not necessarily the password, account, or router port forwarding; DSM’s frontend state and an old port configuration in sshd_config may be layered together.
→
Oct 11, 2026
5 min read
Not Enough Switch Controllers? Atmosphère Users Can Turn a Mac Keyboard or Phone into an Extra Controller
Friends came over but you only have two Joy-Con? If your Switch has Atmosphère and sys-botbase installed, Pocket Gamepad can turn your Mac keyboard or spare phone into a third controller, with a 7-day free trial and a one-time US$1.99 purchase afterward—no need to buy a Pro Controller.
→
← previous
Is Codex Really Secretly Burning Through Your SSD? I Dug Through My Own Machine
next →
Let Claude Code Automatically Generate Images with Your ChatGPT Subscription: No API Key, and How It Gets Around Turnstile

Comments

Replies are public immediately and may be moderated for policy violations.

Max 1000 characters.