terrably
Distribution

Schema migration

Increment schema version, implement upgrade() to migrate stored state, and common patterns for renaming, splitting, and removing attributes.

When to bump schema version

Every resource's Schema carries an integer version (default 0). Terraform compares the version stored in state against the current one. If they differ, it calls Resource.upgrade() once for each intervening version. Additionally, read Terraform's state upgrade documentation.

Bump the version whenever the shape of stored state changes –

ChangeBump?
Rename an attributeYes
Remove an attributeYes
Change an attribute's typeYes
Add a new attribute (optional: true, computed: true)No — see Adding new attributes
Bug fix, description changeNo

Implementing upgrade()

src/resources/server.ts
// Before (v0)
getSchema(): Schema {
  return new Schema([
    new Attribute("id",     types.string(), { computed: true }),
    new Attribute("zone",   types.string(), { required: true }), // ← will be renamed
  ], [], 0);
}

// After (v1) — "zone" renamed to "availability_zone"
getSchema(): Schema {
  return new Schema([
    new Attribute("id",                 types.string(), { computed: true }),
    new Attribute("availability_zone",  types.string(), { required: true }),
  ], [], 1); // ← bump to 1
}

upgrade(ctx: UpgradeContext, version: number, old: State): State {
  switch (version) {
    case 0: {
      // v0 → v1: rename "zone" to "availability_zone"
      const { zone, ...rest } = old;
      return { ...rest, availability_zone: zone };
    }
    default:
      ctx.diagnostics.addError(
        "Unknown schema version",
        `Cannot upgrade state from version ${version}.`,
      );
      return old;
  }
}

Chaining multiple upgrades

When shipping a second breaking change after v1, add a new case and bump to v2. Terraform calls upgrade() once per stored version, chaining through each case in sequence.

upgrade(ctx: UpgradeContext, version: number, old: State): State {
  switch (version) {
    case 0: {
      // v0 → v1: rename "zone" → "availability_zone"
      const { zone, ...rest } = old;
      // Recursively process the next version
      return this.upgrade(ctx, 1, { ...rest, availability_zone: zone });
    }
    case 1: {
      // v1 → v2: split "size_gb" into "disk_size_gb" + "disk_type"
      const { size_gb, ...rest } = old;
      return { ...rest, disk_size_gb: size_gb, disk_type: "ssd" };
    }
    default:
      ctx.diagnostics.addError("Unknown schema version", `Cannot upgrade from v${version}.`);
      return old;
  }
}

Common migration patterns

case 0: {
  const { old_name, ...rest } = old;
  return { ...rest, new_name: old_name };
}
case 0: {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  const { deprecated_field, ...rest } = old;
  return rest;
}
// string → number
case 0: {
  return {
    ...old,
    port: Number(old["port"] as string),
  };
}
// Split one attribute into two
case 0: {
  const { endpoint, ...rest } = old;
  const url = new URL(endpoint as string);
  return {
    ...rest,
    host: url.hostname,
    port: Number(url.port) || 443,
  };
}

Adding a new attribute does not require a version bump if you declare it as optional: true, computed: true with a sensible default. Existing state objects read back null; your read() backfills the value from the live API on the next refresh.

// NEW attribute — existing state is valid because it's optional+computed
new Attribute("disk_type", types.string(), {
  optional: true,
  computed: true,
  default: "ssd",
})
read(ctx: ReadContext, current: State): State {
  const live = await this.api.getServer(current["id"] as string);
  return {
    ...current,
    ...live,
    // Backfill on first read of a resource created before this attribute existed
    disk_type: live.diskType ?? current["disk_type"] ?? "ssd",
  };
}

Semantic versioning

Follow Semantic Versioning with a v prefix (v1.0.0).

ChangeBump
Breaking schema change (removed/renamed attribute, type change)MAJOR
New resource, data source, or attributeMINOR
Bug fix, performance improvementPATCH

Last updated on

On this page