Skip to content

docs(agents): add a tenant-migration skill for moving meshTenants between platforms - #260

Open
grubmeshi wants to merge 3 commits into
mainfrom
feature/tenant-migration-skill
Open

docs(agents): add a tenant-migration skill for moving meshTenants between platforms#260
grubmeshi wants to merge 3 commits into
mainfrom
feature/tenant-migration-skill

Conversation

@grubmeshi

Copy link
Copy Markdown
Collaborator

Adds a tenant-migration skill and its reference material, capturing how to move meshTenants between meshPlatforms without destroying the cloud resources behind them.

Why

Moving a meshTenant to a different meshPlatform looks like an import and is not one. On a custom platform whose landing zone carries a mandatory building block definition, both routes are closed:

  • meshStack rejects a create carrying spec.localId with CustomPlatformImportUnsupportedException, because the building block that is supposed to create the tenant would have to run the equivalent of a terraform import to construct its initial state.
  • Terraform cannot do it either: spec.platform_ref is RequiresReplace, replacing a meshTenant destroys its building block, and that block's teardown destroys the live cloud resource.

The way through is adoption — create the tenant plainly, then write the link to the existing resource into the new block's Terraform state before its runner starts. That is not obvious, it is easy to get wrong in ways that cost live resources, and nothing in this repo wrote it down.

What this is based on

A migration of nine meshTenants off a hand-built custom STACKIT platform onto one deployed by the STACKIT landing zone reference architecture, on a demo meshStack instance. Seven tenants migrated keeping their original project ids, two were deleted as never replicated, all seven building blocks ended SUCCEEDED, and no duplicate project was created at any point.

Every claim in these files was verified against the live instance and the STACKIT API during that work. The failures are included, because they are more instructive than the procedure:

  • Access parity fails whole runs in both directions. A grant the run wants but the seed lacks returns a duplicate error that fails the entire apply. A grant the seed has but the run does not want gets destroyed — on a platform project that would strip the operating service accounts. So the set has to be computed from the meshProject's user bindings, not guessed.
  • A project label can make adoption impossible. A module that computes labels = null plans a label removal the cloud provider ignores, and the provider then fails the apply with Provider produced inconsistent result after apply. A protected label cannot be removed through the labels endpoint at all. Lying about labels in the seed does not help, because the runner refreshes before it plans.
  • A failed first run costs more than a red status. The apply dies before output collection, so the new tenant never gets a platform_tenant_id, which breaks every downstream consumer of that attribute.

Contents

File Purpose
.agents/skills/tenant-migration/SKILL.md The decision layer: tenant classes, the per-tenant loop, the risk method, retiring the emptied platform
.agents/references/tenant-migration-runbook.md The per-tenant procedure, seed construction, access parity, the race and both recovery paths
.agents/references/building-block-state-doctoring.md Reaching and rewriting a running block's Terraform state, and what state edits cannot fix
.agents/references/meshstack-api-cookbook.md Endpoints, media types, validation rules, and the traps that read as empty results
.agents/references/tenant-migration-case-study.md The worked migration — evidence rather than instruction

Two findings worth reading even if you never migrate a tenant

UNPUBLISHED is unreachable for a platform that has ever been published. Three API guards combine so that unpublishing requires restriction = PRIVATE, and PRIVATE is permanently forbidden once wasOncePublished is true. Deactivating a platform's landing zones is the effective retirement, since a meshTenant cannot exist without one. Deleting the platform burns its identifier permanently even though the delete is soft.

Grepping a repository cannot tell you whether a kit/-style module is dead. A building block definition reaches code by Git repository path, so the answer lives on the instance. Read sources per version, not per definition — one definition observed here pointed at three different paths across v1–v37 — and check which versions active blocks are actually pinned to.

Commits

Deliberately two, so the raw capture stays reviewable separately from the editing:

  1. The dump, written while the session was fresh.
  2. The refinement into a skill that reads as an instruction rather than a session log.

Review notes

Documentation only — no module, provider or Terraform changes, so nothing here affects any deployment. .claude/ is a symlink to .agents/, so both tool families pick these up.

🤖 Generated with Claude Code

grubmeshi and others added 2 commits August 19, 2026 10:23
…m migration

Raw capture of a platform engineering session that moved nine meshTenants off a
hand-built custom STACKIT platform onto one deployed by the landing zone
reference architecture, without destroying the projects behind them.

This is the unrefined material, written down while it is still fresh:

- skills/tenant-migration/SKILL.md — first draft of the reusable skill
- references/tenant-migration-runbook.md — the per-tenant procedure
- references/building-block-state-doctoring.md — reaching and rewriting a
  running block's Terraform state
- references/meshstack-api-cookbook.md — endpoints, media types, validation
  rules and the traps that read as empty results
- references/tenant-migration-case-study.md — the worked migration, including
  both failures and what they taught

The next commit turns this into a skill that reads like an instruction rather
than a session log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The raw capture read as a session log: the SKILL.md restated the runbook, the
method was buried below the narrative, and the tool-like material was hard to
reach from the place an agent starts.

Restructure it as a decision layer over the references:

- SKILL.md now opens with the reason adoption is the only path, the two rules
  that prevent an unrecoverable mistake, a six-step workflow, the tenant class
  table, and a per-tenant loop that gives the actual purge, lookup and state-push
  commands instead of describing them. Detail that belongs to one step moved into
  the reference it belongs to.
- The risk method — throwaway plus snapshot, assert on the plan, prefer in-place
  over replacement, weigh the blast radius — is a named section with an
  executable plan gate, so it cannot be skipped by an operator who is tired.
- The runbook leads with the procedure and closes with the three guards that rule
  out an import, since the reader arriving from SKILL.md already knows why.
- State doctoring leads with the adopt harness, the recommended path, and spells
  out both harness setups as commands.
- The cookbook states the `application/vnd.meshcloud.api.` prefix the media type
  table only implied, and shows one complete request.
- The case study stays as evidence and is labelled as such.
- AGENTS.md points at the skill, matching how it indexes `module` and `e2e-test`.

Every technical claim is unchanged; the description field now names the concrete
situation (`CustomPlatformImportUnsupportedException`, `RequiresReplace` on
`spec.platform_ref`) so only an agent facing this problem loads the skill.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Scorecard Check

No module changes detected relative to origin/main.

@aws-amplify-eu-central-1

Copy link
Copy Markdown

This pull request is automatically being deployed by Amplify Hosting (learn more).

Access this pull request here: https://pr-260.d1o16zfeoh2slu.amplifyapp.com

The cookbook told readers to look at `spec.source.terraform.*` on a building block
definition version. The field is `spec.implementation.terraform.*`, and the
difference is not benign: `spec.source` returns null, which reads as "this
definition has no source" rather than as a wrong query.

Found by re-running the audit against a different set of definitions, where every
source came back empty until the field name was corrected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant