Subchapter 36.21
references/bedrock-kb-migration/implement-kb.mdMarkdown30 KBView on GitHub
The Implement phase of a KB-path migration. It turns the approved plan into the new
Managed KB (MANAGED) and its data sources, . Instead of firing one-at-a-time CLI calls, the agent that creates the owned resources in rate-limit-aware batches, previews
it with a no-mutation DRY_RUN, and runs it only after explicit approval of that run.
Scripts
Fetch Bedrock AgentAssets
Kb Shim PyThis phase is mutating, under the strictest controls in the flow. It provisions only:
it creates the new Managed KB and its data sources, owned by the migration, in the source KB’s
Region. It does not ingest, retrieve, validate, cut over, or decommission — those are user-run
handoff steps performed after provisioning completes (see the Handoff phase). The source
self-managed KB (VECTOR; the host skill calls this a Customer-managed KB) is never touched.
Read mapping (opens in a new tab) for the substitutions, managed KB API (opens in a new tab) for request shapes, error→fix, connector auth and batching, and safety-and-iam (opens in a new tab) for the owned-resource ledger, IAM policies and the public-docs reading rule. Honor the explanation style chosen at Identify (technical = exact calls/fields; plain = what each step builds and why); it is presentation only — the script and every real run are still shown in full and run only on explicit per-run approval.
owned_resources ledger
keyed by resource id/ARN. Every mutating call (create-*, put-*) goes through a single helper
that enforces ownership before it touches the SDK — a mutation whose target is not the planned,
owned resource (the source KB or any of its data sources, or any pre-existing/discovered resource)
raises before the SDK call is made, never after. create-* is the one action that produces a new
owned entry: it may target only the planned target (recognisable by the naming_convention and the
ownership tag it sets inline on the create call, not via a separate TagResource call), and the
returned id/ARN + the client token are persisted to the ledger immediately, before any later
operation references them. There is no separate tag step and no update-* in the set.DRY_RUN = True and needs --execute. Put the preview’s output itself (the exact
calls and the resources it would create, pasted as the script printed them, not a checksum or a
summary of it) in your reply and end your turn there. Approval counts only
when the customer gives it after seeing that preview; “approved in advance”, “don’t show me the
preview” or a hurry approves nothing. Never run --execute in the same turn as the preview it executes.start-ingestion-job. Ingestion and retrieval validation are user-run handoff steps on the new
(owned) data sources after provisioning completes.CONTROL_PLANE_RPS, batch size, backoff).connectionConfiguration.secretArn).Checked before any mutation. If unmet, the phase returns NEEDS_INPUT or BLOCKED (see
Phase outcome).
plan_generated == true, with a confirmed source_kb config for every selected KB on a bulk
migration — the whole set is provisioned by this one run (one KNOWLEDGE_BASES entry per KB, never
one Implement run per KB), a confirmed target, and a naming_convention. If the plan or naming
convention is missing, return NEEDS_INPUT.output_dir. A missing artifact is not a COMPLETE — do not generate creates
from memory.owned_resources ledger and confirm it
against the account with read-only list-knowledge-bases / list-data-sources (match by
naming_convention names), then confirm each ledger ID carries this migration’s ownership tag with
list-tags-for-resource (the list and get calls do not return tags; policy B grants the tag read on
tagged KBs). Any resource already ACTIVE / AVAILABLE from a prior run and on the ledger is
reused, not recreated — the script skips creates whose target exists and fills gaps. With the plan’s
client tokens, an interrupted build is safe to resume, not duplicate. A planned name that already
exists but is not on the ledger (or lacks the ownership tag) is BLOCKED — never reused, never renamed
silently; report it and ask.aclEnabled) is undecided.simulate-principal-policy check in safety-and-iam before showing the --execute
command. If write is not available, or the check shows the original is not protected (an Admin-only
session included), return BLOCKED.output_dir is the working directory from Identify; the generated script and any logs go there.boto3 available to run the script; if not, say so and hand the script to the
user to run (still never run it unapproved). The handoff contains everything it refers to: the plan file
(or its path and content), the build policy to attach, and the prerequisites that must exist first (for
example the execution role and its policies), each written out — never “the plan above” when the plan
was not shown.Verify — do not hallucinate.
SEMANTIC / HIERARCHICAL chunking → managed default; top-level CUSTOM type → CUSTOM
connector). Read it before generating; never re-derive a rule from memory. On a run-time rejection, use
the managed KB API (opens in a new tab) error→fix index rather than
guessing.create-* shape against public docs (read per
safety-and-iam (opens in a new tab)) and/or
aws bedrock-agent <command> --generate-cli-skeleton before writing it into the script — the template
bodies are sketches. Do not invent parameter names.MANAGED needs no model (omit embeddingModelArn; keeps the managed
reranker; all documents re-embedded). For bring-your-own use embeddingModelType: CUSTOM — fetch the
supported set from the managed-KB docs (do not hardcode a stale list) and confirm Region. A CUSTOM
text model must use the managed dimension and data type from the create page (else rejected), disables
the managed reranker, and cannot change
after creation. Exact shape: managed KB API (opens in a new tab).create-data-source --generate-cli-skeleton for the user’s Region before a real run.FAILED): the managed shape differs from self-managed —
the secret is connectionConfiguration.secretArn (not credentialsSecretArn); no
accessControlConfiguration / crawlAcl (use the flat connectorParameters.aclEnabled); no
enableDeletionProtection (use deletionProtectionConfiguration.deletionProtectionStatus); connector
names are WEB / GOOGLEDRIVE (not WEB_CRAWLER / GOOGLE_DRIVE); SharePoint ENTRA_ID_APP_ONLY needs
a certificateS3Path even with ACLs off; siteUrls is a JSON array; Confluence hostUrl ends
.atlassian.net with nested connectionConfiguration.type: SAAS.Bedrock build-time control-plane APIs are throttled per-account/Region, and there is a hard limit on data
sources per KB (quota L-F02D918A, read live; one past it → ServiceQuotaExceededException; split across KBs). Firing creates
back-to-back invites ThrottlingException, which is why a batched script is used instead of one-by-one CLI.
The script’s constants (CONTROL_PLANE_RPS, DATA_SOURCE_BATCH_SIZE, INTER_BATCH_PAUSE_SECONDS) are
conservative defaults — read the per-KB and per-account quotas from kb-managed-quotas and the
Service Quotas console before execution, lower the rate if throttling
appears, and never hardcode an unverified quota as fixed truth.
The script reads data, not prose. Write two JSON files to output_dir before generating the script:
migration-plan.json: migration_id, region, ownership_tag (key and value), source_kb_ids (guard
only), and one entry per planned KB with its planned name, client_token, the create request body, and
its data sources (each with name, client_token and request body). Discovered free text (a KB
description, a data source description) goes in this file only as JSON data, never into Python source.owned-resources.json (the ledger): every resource this migration created, keyed by ID, with its type,
name and client token; execution roles with the names of their inline policies, so a later teardown can
remove the policies before the role.Approval is bound to the previewed plan. The preview prints the SHA-256 of migration-plan.json;
--execute requires --expect-plan-sha256 <that value> and refuses, before any SDK call, when it is missing
or does not match the file on disk. A changed plan therefore needs a fresh preview and a fresh approval.
The agent adapts a template into a concrete script written to <output_dir>/provision_managed_kb.py (or a
user-chosen path — never silently). The template is not shipped; it is specified here. The generated
script MUST:
DRY_RUN = True at module top; --execute is required to mutate. The DRY_RUN
run prints the intended calls, mutates nothing, and needs no credentials. The preview lists exactly the
planned creates — the KBs and data sources in the plan, with their names and shapes — and nothing else:
a role, policy or other resource the plan leaves to the customer never appears in it.CreateKnowledgeBase → batched create_data_source → poll
GetKnowledgeBase until ACTIVE / GetDataSource until AVAILABLE. storageConfiguration is
omitted (auto-provisioned on MANAGED). No start-ingestion-job, no retrieve, no cut-over.ACTIVE than one on the AWS-owned key (observed in one run: many times longer). Size the poll window
for it, and treat a poll timeout as FAILED that a re-run resumes (the re-run reconciles the ledger).ACTIVE; then issue
create-data-source for every KB in batches of ~10 (DATA_SOURCE_BATCH_SIZE) with an inter-batch pause;
then poll the whole set to AVAILABLE. Owned-only, provisioning only.CONTROL_PLANE_RPS plus bounded exponential backoff with
random jitter on ThrottlingException / 429: the wait grows with each attempt, carries a random
component, and stops after a fixed number of attempts with a clear failure naming the call. Layer it over
boto3’s adaptive retries; fixed sleeps between calls are pacing, not backoff.clientToken once, at plan time, and write it into the plan
next to the create it belongs to: mig-<prefix>-<hash>-<seq>, where <hash> is the first 24 hex characters
of SHA-256 over <migration_id>|<resource type>|<planned name>. Letters, digits and hyphens only, ≥33
chars. The script reads each token from the plan and never generates one (no uuid.uuid4(), no random
source), so the same create sends the same token on every run: a retry never creates a duplicate and an
interrupted run resumes cleanly.connectionConfiguration.secretArn; a secret value is never
written or printed.CreateKnowledgeBase, and report all violations at once — so an over-quota KB late in the list
never strands earlier KBs as ACTIVE-but-empty with no rollback path.naming_convention (e.g. <original>-managed), not ad-hoc strings — the
idempotency/reconciliation check keys off these names to avoid recreating a prior run’s resource.REGION, PROFILE, source KB id (guard/reference only), KB execution role
ARN, embedding model (per the rule above), and one KNOWLEDGE_BASES entry per KB with data sources
already translated to MANAGED-accepted values. An unfilled script with placeholders left in is not
valid: if a value is unknown (a profile name, a role ARN), ask the customer for it, or do not write the
script yet. Never write <FILL_IN_...> or a similar marker.The ownership guard is not advisory prose — the generated script MUST refuse a non-owned target
before the boto3 call and MUST persist each created id/ARN and its client token to the ledger the instant
the create returns. The two creates are not interchangeable: CreateKnowledgeBase accepts inline tags
and returns knowledgeBase.knowledgeBaseId; CreateDataSource accepts no tags field and returns
dataSource.dataSourceId. Generate two distinct helpers, not one generic wrapper (adapt names/values;
keep the control flow):
# ---- Owned-resource ledger: the ONLY record a mutation may target ----
owned_kb_ids = {} # knowledgeBaseId -> {"name", "client_token"} (populated as KBs are created)
owned_ds_ids = {} # dataSourceId -> {"kb_id", "name", "client_token"}
PLANNED = load_plan(output_dir) # planned KB/DS *names* + naming_convention + ownership tag
PLANNED_KB_NAMES = PLANNED.kb_names # set[str] of planned KB names
PLANNED_DS_NAMES = PLANNED.ds_names # set[str] of planned data-source names
SOURCE_KB_IDS = PLANNED.source_kb_ids # set[str] of source KB *ids* — never a mutation target
OWNERSHIP_TAG = PLANNED.ownership_tag
Rules the structure encodes, each of which the script MUST honor:
assert_planned_name / assert_owned_kb raise before the boto3 call. A
create whose name is not a planned target, and any data source whose parent KB is not already an owned,
persisted id — especially a source KB — raises NotOwned and never reaches the service.SOURCE_KB_IDS. Never test a name for membership in an
id set (or vice versa) — they never collide and the check would be vacuous.create_knowledge_base_owned sends the inline ownership tags and reads
knowledgeBase.knowledgeBaseId; create_data_source_owned sends no tags field and reads
dataSource.dataSourceId. Do not route both through one helper that passes tags to a data-source create.create_data_source_owned resolves its kb_id from that persisted ledger, so
an id that was never persisted cannot be referenced.DRYRUN-<name> placeholder in
the in-memory ledger (never persisted) so every data-source preview passes assert_owned_kb, and the
Get* polling is printed, not called. The preview needs no credentials and touches no AWS resource.TagResource call. The ownership tag is set in create_knowledge_base_owned‘s
tags argument. Applying that inline tag is authorized by the bedrock:TagResource dependent
permission of CreateKnowledgeBase (policy B grants it for exactly this reason) — but the script issues
no standalone TagResource call. CreateDataSource takes no tags field at all.create_knowledge_base_owned(…) → create_data_source_owned(…) →
read-only Get* polling. No update-*, no start-ingestion-job, no retrieve.The script is fail-fast up front and has no automatic teardown. Its only safety is the up-front quota validation (no KB is created until the whole plan validates) plus the immutable source. There is no teardown function and none is generated.
Do not claim the script tears down automatically. State this posture explicitly when reporting a FAILED
build. When a build stops with resources already created, say in the reply: what this run created (from the
ledger), that nothing is removed automatically, and that a teardown would delete only those resources, never
the original KB or anything that existed before. Then ask whether they want it.
Build → review → DRY_RUN preview → approve that run → execute → confirm → handoff.
If the plan needs an isolation bucket or an execution role for the new KB, ensure they exist first and add
each to owned_resources (or hand off to the user if out of scope for the build profile). Never reuse the
source’s resources for writes.
S3 / Web / CUSTOM need no secret — skip. Confluence / SharePoint / OneDrive / Google Drive each need a
Secrets Manager secret — the #1 cause of FAILED data sources. Before putting such a data source in the
script:
authType.secretsmanager:GetSecretValue on the ARN (+ kms:Decrypt for a CMK; +
s3:GetObject / s3:ListBucket on the .p12 for certificate methods).siteUrls / hostUrl / tenantId are correctly shaped before the create runs. If anything is unmet
or unknown, stop and hand off the secret-creation step — never run the script to “test” auth against the
live service.Produce provision_managed_kb.py per
The provisioning script the agent generates and write it
(show the write, get approval). It must create all KBs first and poll to ACTIVE, then issue batched
create-data-source calls (each with the validated config and its mapping rationale) across all KBs and
poll to AVAILABLE — owned-only, provisioning only.
Show the script (or its key sections). Then run the no-mutation preview (note it changes nothing; run after approval):
python3 <output_dir>/provision_managed_kb.py # DRY_RUN=True: prints intended calls, mutates nothingUse it to confirm request shapes, batch sizing and rate-limit settings before any mutation.
Show the real command, label it MUTATING, and run it only after approval of that run:
python3 <output_dir>/provision_managed_kb.py --execute # creates KB + data sources (no ingestion)It creates all KBs, waits for every one ACTIVE, then all data sources in rate-limited batches and waits
for every one AVAILABLE. On a throttling/validation error, consult the
managed KB API (opens in a new tab) error→fix index. For a FAILED data
source, do one diagnosis pass (secret/keys/Region/ARN, role perms, siteUrls array), report the
specific missing item, and stop. Do not loop on FAILED.
If the previewed script changed, re-approve. Any edit to the script or its inputs after the DRY_RUN invalidates the previous approval — re-run the DRY_RUN preview and get a fresh explicit approval of the new run before executing. Approval is bound to the exact run that was previewed.
Independently confirm with pre-approved read-only checks:
aws bedrock-agent get-knowledge-base --region <REGION> --knowledge-base-id <new-owned-kb> --query 'knowledgeBase.status'
aws bedrock-agent list-data-sources --region <REGION> --knowledge-base-id <new-owned-kb>
aws bedrock-agent get-data-source --region <REGION> --knowledge-base-id <new-owned-kb> --data-source-id <new-owned-ds> --query 'dataSource.status'Verify each KB ACTIVE and every data source AVAILABLE (or report the error state).
Hand off only after all planned KBs are ACTIVE and every data source AVAILABLE. Ingestion (per data
source, to COMPLETE), retrieval validation, cut-over and decommission are all user-run handoff steps —
surface them as the user’s next actions, each with a public doc link (verification runs the same
representative queries against the source and new KBs and compares ranking and content, not score magnitude,
Retrieve against Retrieve; the source is kept until the user is satisfied). Never run them here
(invariant 3).
Report every owned resource and its readiness, the script path, the naming convention applied, resources reused from a prior run, and the substitutions applied; then yield to the Handoff phase.
The phase ends in one of COMPLETE | NEEDS_INPUT | BLOCKED | FAILED.
COMPLETE — all planned KBs ACTIVE, all data sources AVAILABLE; provisioning script written. Named
outputs the next phase needs: the new KB id(s) (one per created KB — a bulk run lists them all), the data
source id(s), naming_convention, resources reused, substitutions applied, and the script path.NEEDS_INPUT — a precondition (plan, naming convention, secret) is missing; relay it to the user.BLOCKED — credential posture does not permit the build; stop.FAILED — a mid-run failure. Report the specific cause, list the owned resources created so far from the
ledger, and offer an owned-only teardown as a separate approved step (there is no automatic
teardown; rollback for the user is keeping the untouched original).Phase completion means the required artifacts are present and non-empty on disk and the user confirmed —
confirming a file merely exists is not the same as confirming the build reached ACTIVE / AVAILABLE.
If asked to run the script (or any mutating command) without approval of that run, to modify/sync/delete/ingest the source KB, to perform cut-over, or to decommission the original — refuse, explain that this phase only writes and (on approval) runs a script provisioning migration-owned resources, and stop.
storageConfiguration; HIERARCHICAL / DEFAULT /
SEMANTIC chunking; any chunking config (incl. FIXED_SIZE/NONE) when the KB uses the managed
embedding model (“A chunking strategy cannot be specified with a managed embedding model” —
omit it; a chunking config is valid only with a custom embedding model); top-level
type: CUSTOM instead of the CUSTOM connector; a self-managed connector
field (crawlAcl, credentialsSecretArn, enableDeletionProtection, WEB_CRAWLER / GOOGLE_DRIVE)
instead of the managed shape.CONTROL_PLANE_RPS), more data sources on one
KB than its quota (split), or hardcoding an unverified quota.FAILED cause): wrong connectionConfiguration.secretArn, secret keys not
matching the managed authType, SharePoint ENTRA_ID_APP_ONLY missing certificateS3Path, KB role
lacking secretsmanager:GetSecretValue / kms:Decrypt (or S3 cert read), or siteUrls as a string
rather than a JSON array.