# Execution package

Generated by StackToIT from `execution-plan.json`. Nothing in this folder has been run: StackToIT reads and writes files and creates, changes and deletes nothing in any cloud. Every step is carried out by a person, by OpenTofu or by a script of this folder that a person runs. Paths are relative to the output folder.

Partially executable migration package · For review only · Not approved.

**What will be created.** OpenTofu creates 10 resource blocks; the scripts of this package make 2 STACKIT API calls. Architect work: 64 manual steps in 24 work packages.

**What can run as written.** 10 of the 12 automated steps (OpenTofu or a script) are free of decisions and manual prerequisites (2 require a design decision) · 0 can run as written · 10 of those 10 wait for a value (project_id, containers_machine_type, containers_nodes_max, containers_nodes_min and 7 more); a person can start 29 of the 64 manual steps now (10 wait for a manual prerequisite, 3 wait for an automated step, 22 require a design decision); 2 steps of the plan are permanently unsupported (refused).

**Readiness levels.** 12 automated steps are in the draft · 10 are free of decisions and manual prerequisites · 0 rest on no default to confirm, no opt-in to answer and no value to supply · 0 are approved (every default and decision they rest on is confirmed in the decisions file, and no value they read is left to supply) · 10 of the steps free of decisions and manual prerequisites wait for a value (project_id, containers_machine_type, containers_nodes_max, containers_nodes_min and 7 more).

**What the figures count.** Three measures, not one: "can run as written" means the step is free of decisions and manual prerequisites and waits for no value (0 of the 12 automated steps); "automation-eligible" means the critical settings of the unit are closed (0 of 11 units; 11 cannot be automated in this build: a provider the build cannot check (11 units) and 5 more reasons, every one on the Automation eligibility line of index.html); "waiting for a value" means the step is free of decisions and manual prerequisites and reads a variable that has no value yet, so a person supplies it before the step runs and the step is never approved before (10 of the 10 steps free of decisions and manual prerequisites: the plan lists 11 OpenTofu variables, the STACKIT project id among them, with no value).

**What cannot be preserved.** 72 critical settings of 16 source resources that the draft does not carry are open as manual or refused steps ([engineering/not-carried.csv](../engineering/not-carried.csv) lists what the draft does not carry, property by property). 2 steps of the plan are refused: the target has no equivalent (see What cannot be migrated).

**Decisions required.** 5 decisions (9 questions) must be answered before the steps that wait for them can be carried out ([Required decisions](#required-decisions); described in [executive/decisions.md](../executive/decisions.md)). These are the decisions that steps of the plan wait for; the decisions to make of the register (decisions.json) also count the manual actions and plan statements that need a person, so that number can be larger.

**First work package.** [`WP-01`](manual/actions.md), Set up the identities and access (prepare) — owner: Identity Architect, 4 manual steps. The next package that waits, `WP-04` (Secrets migration user retire (security)), waits for decisions. It opens when `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password` is answered.

## Work packages

The 64 manual steps of this plan in 24 work packages: the steps of one phase that one role carries out together, of one kind of work. `execution/manual/actions.md` lists each package with its steps in full and `migration/work-packages.csv` has one row per package; every step is also listed once under Execution order. A package is as far as its least advanced step.

| Package | Owner | Phase | Steps | Resources | Decisions to answer | State |
|---|---|---|---|---|---|---|
| [`WP-01`](manual/actions.md) Set up the identities and access (prepare) | Identity Architect | prepare | 4 | 4 | — | a person can start it now |
| [`WP-02`](manual/actions.md) Decide and set the critical settings (security) | Identity Architect | security | 6 | 6 | — | a person can start it now |
| [`WP-03`](manual/actions.md) Decide and set the critical settings (security) | Security Architect | security | 2 | 2 | — | a person can start it now |
| [`WP-04`](manual/actions.md) Secrets migration user retire (security) | Security Architect | security | 1 | 2 | `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password` | waits for decisions |
| [`WP-05`](manual/actions.md) Secrets read back (security) | Security Architect | security | 1 | 2 | `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password` | waits for decisions |
| [`WP-06`](manual/actions.md) Decide and set the critical settings (data services) | Identity Architect | data services | 2 | 2 | — | a person can start it now |
| [`WP-07`](manual/actions.md) Decide and set the critical settings (data services) | Network Architect | data services | 1 | 1 | — | a person can start it now |
| [`WP-08`](manual/actions.md) Decide and set the critical settings (data services) | Platform Architect | data services | 4 | 4 | `DEC-GEN-CONVERT-SOURCE-REGION-MAPPING`, `DEC-GEN-CONVERT-SOURCE-PROJECT-MAPPING` | waits for decisions (2 of 4 manual steps) |
| [`WP-09`](manual/actions.md) Decide and set the critical settings (data services) | Security Architect | data services | 1 | 1 | — | a person can start it now |
| [`WP-10`](manual/actions.md) Decide and set the critical settings (data services) | Storage Architect | data services | 1 | 1 | — | a person can start it now |
| [`WP-11`](manual/actions.md) Decide on the switched-off modules that hold a STACKIT alternative of what has no equal (data services) | Network Architect | data services | 1 | 1 | `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_firebase_hosting_site.client` | waits for decisions |
| [`WP-12`](manual/actions.md) Move the data and the objects to the target | Data Architect | data services | 8 | 2 | `DEC-GEN-CONVERT-DATA-MOVEMENT` | waits for decisions (2 of 8 manual steps) |
| [`WP-13`](manual/actions.md) Decide and set the critical settings (platform services) | Identity Architect | platform services | 2 | 2 | — | a person can start it now |
| [`WP-14`](manual/actions.md) Decide and set the critical settings (platform services) | Network Architect | platform services | 2 | 2 | — | a person can start it now |
| [`WP-15`](manual/actions.md) Decide and set the critical settings (platform services) | Platform Architect | platform services | 3 | 3 | — | a person can start it now |
| [`WP-16`](manual/actions.md) Decide and set the critical settings (platform services) | Security Architect | platform services | 2 | 2 | — | a person can start it now |
| [`WP-17`](manual/actions.md) Carry by hand what the STACKIT service for pubsub topic does not take from the draft (platform services) | Platform Architect | platform services | 1 | 1 | `DEC-GEN-CONVERT-API-OPT-IN/google_pubsub_topic.faux` | waits for decisions |
| [`WP-18`](manual/actions.md) Cluster access (platform services) | Platform Architect | platform services | 1 | 2 | — | a person can start it after the automated step it comes after has run |
| [`WP-19`](manual/actions.md) Messaging topology (platform services) | Platform Engineer | platform services | 1 | 1 | — | a person can start it now |
| [`WP-20`](manual/actions.md) Decide on the switched-off modules that hold a STACKIT alternative of what has no equal (workloads) | Platform Architect | workloads | 3 | 3 | `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.activategcb`, `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.init`, `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.placeholder` | waits for decisions |
| [`WP-21`](manual/actions.md) Kubernetes apply (workloads) | Platform Architect | workloads | 2 | 2 | — | a person can start after WP-18 is done |
| [`WP-22`](manual/actions.md) Move the data and the objects to the target | Data Architect | cutover | 8 | 2 | `DEC-GEN-CONVERT-DATA-MOVEMENT` | waits for decisions (6 of 8 manual steps) |
| [`WP-23`](manual/actions.md) Move the data and the objects to the target | Migration Owner | cutover | 4 | 2 | `DEC-GEN-CONVERT-DATA-MOVEMENT` | waits for decisions (3 of 4 manual steps) |
| [`WP-24`](manual/actions.md) Retire the source resources | Data Architect | decommission | 3 | 2 | `DEC-GEN-CONVERT-DATA-MOVEMENT` | waits for decisions (2 of 3 manual steps) |

## How the package is carried out

This is an operator-executable migration plan: a person, OpenTofu or a script that a person runs carries out each step, in the order below, and nothing in this folder runs by itself. OpenTofu applies the OpenTofu steps together in one `tofu apply`, so the manual steps and STACKIT API calls that come before it are not enforced: `execution/terraform/apply.md` lists them as a checklist to complete first. `execution/deploy.sh` is the one entry point that carries the steps out in that order for you, phase by phase up to the verification: with no argument it prints the order and changes nothing, `--run` carries the steps out and stops at each step that waits for a decision, a value, an approval or a person, and running it again resumes from `review/progress.yaml`. StackToIT has not run it. An executor inside StackToIT that would carry the plan out step by step (`stacktoit execute`) is designed and not implemented.

## Verdict

**Partially executable migration package**

Steps: partially executable: some steps cannot be carried out as the plan stands, whatever is decided: they are marked not executable under Execution order.

The verdict is of the 78 steps of the plan of this package. Every resource and data block of the draft (10) is a step of it. Every source resource of the assessment (16) is a step of it, or needs neither a STACKIT resource nor a step (0). These files of the draft hold no step of the plan at all: `terraform/proposals.tf`, `terraform/terraform.tf`, `terraform/variables.tf`.

Execution completeness: COMPLETE (each of the 64 manual steps states what it is, why, who carries it out and when it is done, and the input it needs where it depends on one).

10 of the 12 automated steps (OpenTofu or a script) are free of decisions and manual prerequisites (2 require a design decision) · 0 can run as written · 10 of those 10 wait for a value (project_id, containers_machine_type, containers_nodes_max, containers_nodes_min and 7 more); a person can start 29 of the 64 manual steps now (10 wait for a manual prerequisite, 3 wait for an automated step, 22 require a design decision); 2 steps of the plan are permanently unsupported (refused). Migration-ready: NO — 72 critical settings of 16 source resources have manual or refused steps open (65 manual, 2 refused, 5 with no step); 26 steps of the plan cannot be carried out now (24 wait only for a decision, 2 are refused).

The critical settings of the source that the draft does not carry, by source resource: each is a manual or refused step under Execution order (a source resource the draft writes no resource for carries them in its own step); `assurance/critical-semantics.json` says why each is open and what closes it. A setting the knowledge base names by a generated id is said in words before its id: the source attribute it is, as the step of the setting states it, or else its id in words where the plan states no attribute for it.

- `google_cloud_run_v2_job.migrate` — manual (steps 46, 47, 48, 49, 59): `cloud-run-v2-job-network-exposure`, `deployment-artifact`, `entry-point`, `k4-cloud-run-volume-mount-cloud-run-v2-job`, `secrets`, `service-identity`, its name (`tc-cloud-run-v2-job-name`), the template containers env name of template (`tc-cloud-run-v2-job-template--template--containers--env--name`), its template template volumes name (`tc-cloud-run-v2-job-template--template--volumes--name`), `terraform-destroy-guard`; no step of the plan carries `location-residency`
- `google_cloud_run_v2_service.server` — manual (steps 50, 51, 52, 53): `cloud-sql-connection`, `deployment-artifact`, `environment`, `iam-invocation`, `iam-policy-grant`, `k4-cloud-run-volume-mount-cloud-run-v2-service`, `secrets`, `service-identity`, its name (`tc-cloud-run-v2-service-name`), its template volumes name (`tc-cloud-run-v2-service-template--volumes--name`), `terraform-destroy-guard`; no step of the plan carries `location-residency`
- `google_cloudbuild_trigger.activategcb` — manual (step 61): `build-identity`, `dep-references`, `deployment-artifact`, `entry-point`, `log-destination`, `project-residency`
- `google_cloudbuild_trigger.init` — manual (step 62): `build-identity`, `dep-references`, `deployment-artifact`, `entry-point`, `log-destination`, `project-residency`
- `google_cloudbuild_trigger.placeholder` — manual (step 63): `build-identity`, `dep-references`, `deployment-artifact`, `linked-dependency:google_firebase_hosting_site`, `log-destination`, `project-residency`
- `google_firebase_hosting_site.client` — manual (step 32): its project (`tc-firebase-hosting-site-project`), its site id (`tcr-firebase-hosting-site-site-id`)
- `google_firebase_project.default` — refused (step 55): its project (`tc-firebase-project-project`)
- `google_pubsub_topic.faux` — manual (step 54): its name (`tc-name`)
- `google_secret_manager_secret.django_admin_password` — manual (steps 5, 6): `secret-settings`, `secret-value`, its django admin password (`tcr-secret-id`)
- `google_secret_manager_secret.django_settings` — manual (steps 7, 8): `secret-settings`, `secret-value`, its django settings (`tcr-secret-id`)
- `google_service_account.automation` — manual (step 9): `account-name`, `role-bindings`
- `google_service_account.client` — manual (step 10): `account-name`, `role-bindings`
- `google_service_account.init` — manual (step 11): `account-name`, `role-bindings`
- `google_service_account.server` — manual (step 12): `account-name`, `role-bindings`
- `google_sql_database_instance.postgres` — manual (steps 25, 26, 27): `database-users`, its name (`tc-name`), the name of its google_sql_database (`tcf-sql-database--name`), `user-and-database-settings`; no step of the plan carries `project-scope`, `region-residency`
- `google_storage_bucket.media` — manual (steps 28, 29, 30, 31): `geo-replication`, `iam-access`, `iam-grant`, `public-access`, its name (`tc-storage-bucket-name`); refused (step 33): `force-destroy`; no step of the plan carries `bucket-region`

## What will be created

OpenTofu creates 10 resource blocks; 10 of the OpenTofu steps can be carried out now. The scripts of this package make 2 STACKIT API calls.

- `stackit_objectstorage_bucket`: 1
- `stackit_postgresflex_instance`: 1
- `stackit_rabbitmq_instance`: 1
- `stackit_secretsmanager_instance`: 1
- `stackit_secretsmanager_user`: 1
- `stackit_service_account`: 4
- `stackit_ske_cluster`: 1

## Proposed, switched off

The draft holds these STACKIT alternatives as modules that are off. They are no step of this package and no count above holds them: each stands for a source resource that keeps its manual step, or for a capability of a resource the draft writes, and a plan of the draft holds nothing of it until a person decides on it, gives it its values and sets its switch to true.

### Proposed: Cloud Build trigger: starts a build on a source event, a Pub/Sub message, a webhook or by hand, with its steps, substitutions, artifacts, secrets and optional approval

- Stands for: `google_cloudbuild_trigger.activategcb`
- Module: `terraform/proposals/proposal_activategcb`, switched on by `proposal_activategcb_enabled`
- Not equal to the source: Nothing is converted. The same delivery is a workflow of STACKIT Git Pipelines (written for GitHub Actions; STACKIT-managed or custom runners; secrets from Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Trigger conditions, steps, secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person.
- What you decide: Which pipelines are still needed, where they run (STACKIT Git Pipelines or the customer's own CI/CD), how each trigger, step, secret and approval is written again, and who owns the workflow files.
- Not carried: the source repeats this resource and the module writes it once: give each instance its own design
- Not carried: build.options.logging: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.id: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: build.step.script: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: description: the module takes no value of the source: give the value in the values if it is wanted
- Not carried: location: a region of the source is not copied: the module runs in the region the provider of the draft sets, or the one given in the values where it has a setting for it
- Not carried: name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: pubsub_config.topic: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: service_account: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add

### Proposed: one member's role on the Cloud Storage bucket

- Stands for: google_storage_bucket_iam_member.automation, beside the draft of google_storage_bucket.media; that draft stays as it is and this is no step of it
- Module: `terraform/proposals/proposal_automation`, switched on by `proposal_automation_enabled`
- Not equal to the source: A STACKIT credentials group is not a grant on the Cloud Storage bucket. It is the principal that a bucket policy names, a set of S3 keys with no Google identity behind it, and by itself it gives no access to the bucket and takes none away. The statement that grants it something is a statement of the bucket policy, in the policy language of STACKIT (a credentials group or * as the principal, S3 actions, five condition keys), which no Terraform resource sets. The Google role is not mapped to S3 actions by any table, the condition of the grant has no counterpart unless it can be written with those five condition keys, and a Google group, a domain, a federated identity and allUsers or allAuthenticatedUsers are named again as credentials groups or as *.
- What you decide: Which credentials group stands for each member of the grant, which S3 actions of the bucket policy give the same people the access they need and no more than the Google role gave, what becomes of the condition, of public members (allUsers, allAuthenticatedUsers), of Google groups, domains and federated identities, and whether the grant ends with the Cloud Storage bucket when it has no replacement. Which bucket the grant is on, because the scan does not hold it.
- Not carried: member: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: role: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add

### Proposed: static web hosting site

- Stands for: `google_firebase_hosting_site.client`
- Module: `terraform/proposals/proposal_client`, switched on by `proposal_client_enabled`
- Not equal to the source: Static files are published into an Object Storage bucket by a pipeline, and a CDN distribution (a beta resource) with the bucket as origin serves them on a custom domain; the redirects of the site become redirect rules of the distribution. Custom response headers, rewrites to Cloud Run services or Cloud Functions, preview channels and the release history have no counterpart and are restated in the pipeline or in the application.
- What you decide: Which bucket is the origin, which domain the distribution serves, and how rewrites and previews are replaced. The CDN resources are beta.
- Not carried: the source repeats this resource and the module writes it once: give each instance its own design
- Not carried: project: the module is written for the project of the draft
- Not carried: site_id: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (objectstorage_bucket_name, cdn_distribution_config, cdn_custom_domain_distribution_id, cdn_custom_domain_name), and every other setting is the customer's to add

### Proposed: the members of one role on the Secret Manager secret

- Stands for: google_secret_manager_secret_iam_binding.django_admin_password, beside the draft of google_secret_manager_secret.django_admin_password; that draft stays as it is and this is no step of it
- Module: `terraform/proposals/proposal_django_admin_password`, switched on by `proposal_django_admin_password_enabled`
- Not equal to the source: A STACKIT role assignment is not a grant on the Secret Manager secret. It gives one user, group or service account one STACKIT role on a whole project, folder or organisation. The role is a set of STACKIT permissions, not the Google role, and no table maps one to the other. It has no condition and no public member, so what the grant gave to a Google group, a domain, a federated identity or everyone is named again, and the access reaches more than the Secret Manager secret. A binding on a secret group applies to every secret in the group, so it is wider than a grant on one secret unless the secret has a group of its own, and its roles are Secrets Manager roles. The binding was authoritative for its role: STACKIT role assignments are additive, so a person who holds the role through another assignment keeps it, and removing such people is a step of a person.
- What you decide: Which STACKIT role, on which project, folder or organisation, gives the same people the access they need on the Secret Manager secret, knowing it covers everything in that scope; which STACKIT users, groups or service accounts take the place of Google groups, domains, federated identities and a public member; and whether the grant ends with the Secret Manager secret when it has no replacement. For secrets: which secret group holds the secret, who else shares that group, and which Secrets Manager role and subject take the grant. Whether people who hold the role through another assignment keep it.
- Not carried: members: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (authorization_project_role_assignment_resource_id, authorization_project_role_assignment_role, authorization_project_role_assignment_subject, authorization_folder_role_assignment_resource_id, authorization_folder_role_assignment_role, authorization_folder_role_assignment_subject, authorization_organization_role_assignment_resource_id, authorization_organization_role_assignment_role and 7 more), and every other setting is the customer's to add
- Not carried: role: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (authorization_project_role_assignment_resource_id, authorization_project_role_assignment_role, authorization_project_role_assignment_subject, authorization_folder_role_assignment_resource_id, authorization_folder_role_assignment_role, authorization_folder_role_assignment_subject, authorization_organization_role_assignment_resource_id, authorization_organization_role_assignment_role and 7 more), and every other setting is the customer's to add

### Proposed: the members of one role on the Secret Manager secret

- Stands for: google_secret_manager_secret_iam_binding.django_settings, beside the draft of google_secret_manager_secret.django_settings; that draft stays as it is and this is no step of it
- Module: `terraform/proposals/proposal_django_settings`, switched on by `proposal_django_settings_enabled`
- Not equal to the source: A STACKIT role assignment is not a grant on the Secret Manager secret. It gives one user, group or service account one STACKIT role on a whole project, folder or organisation. The role is a set of STACKIT permissions, not the Google role, and no table maps one to the other. It has no condition and no public member, so what the grant gave to a Google group, a domain, a federated identity or everyone is named again, and the access reaches more than the Secret Manager secret. A binding on a secret group applies to every secret in the group, so it is wider than a grant on one secret unless the secret has a group of its own, and its roles are Secrets Manager roles. The binding was authoritative for its role: STACKIT role assignments are additive, so a person who holds the role through another assignment keeps it, and removing such people is a step of a person.
- What you decide: Which STACKIT role, on which project, folder or organisation, gives the same people the access they need on the Secret Manager secret, knowing it covers everything in that scope; which STACKIT users, groups or service accounts take the place of Google groups, domains, federated identities and a public member; and whether the grant ends with the Secret Manager secret when it has no replacement. For secrets: which secret group holds the secret, who else shares that group, and which Secrets Manager role and subject take the grant. Whether people who hold the role through another assignment keep it.
- Not carried: members: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (authorization_project_role_assignment_resource_id, authorization_project_role_assignment_role, authorization_project_role_assignment_subject, authorization_folder_role_assignment_resource_id, authorization_folder_role_assignment_role, authorization_folder_role_assignment_subject, authorization_organization_role_assignment_resource_id, authorization_organization_role_assignment_role and 7 more), and every other setting is the customer's to add
- Not carried: role: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (authorization_project_role_assignment_resource_id, authorization_project_role_assignment_role, authorization_project_role_assignment_subject, authorization_folder_role_assignment_resource_id, authorization_folder_role_assignment_role, authorization_folder_role_assignment_subject, authorization_organization_role_assignment_resource_id, authorization_organization_role_assignment_role and 7 more), and every other setting is the customer's to add

### Proposed: Cloud Build trigger: starts a build on a source event, a Pub/Sub message, a webhook or by hand, with its steps, substitutions, artifacts, secrets and optional approval

- Stands for: `google_cloudbuild_trigger.init`
- Module: `terraform/proposals/proposal_init`, switched on by `proposal_init_enabled`
- Not equal to the source: Nothing is converted. The same delivery is a workflow of STACKIT Git Pipelines (written for GitHub Actions; STACKIT-managed or custom runners; secrets from Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Trigger conditions, steps, secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person.
- What you decide: Which pipelines are still needed, where they run (STACKIT Git Pipelines or the customer's own CI/CD), how each trigger, step, secret and approval is written again, and who owns the workflow files.
- Not carried: the source repeats this resource and the module writes it once: give each instance its own design
- Not carried: build.options.logging: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.id: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: build.step.script: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: description: the module takes no value of the source: give the value in the values if it is wanted
- Not carried: location: a region of the source is not copied: the module runs in the region the provider of the draft sets, or the one given in the values where it has a setting for it
- Not carried: name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: pubsub_config.topic: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: service_account: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add

### Proposed: Cloud Build trigger: starts a build on a source event, a Pub/Sub message, a webhook or by hand, with its steps, substitutions, artifacts, secrets and optional approval

- Stands for: `google_cloudbuild_trigger.placeholder`
- Module: `terraform/proposals/proposal_placeholder`, switched on by `proposal_placeholder_enabled`
- Not equal to the source: Nothing is converted. The same delivery is a workflow of STACKIT Git Pipelines (written for GitHub Actions; STACKIT-managed or custom runners; secrets from Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Trigger conditions, steps, secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person.
- What you decide: Which pipelines are still needed, where they run (STACKIT Git Pipelines or the customer's own CI/CD), how each trigger, step, secret and approval is written again, and who owns the workflow files.
- Not carried: the source repeats this resource and the module writes it once: give each instance its own design
- Not carried: build.options.logging: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.env: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.id: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: build.step.name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: description: the module takes no value of the source: give the value in the values if it is wanted
- Not carried: location: a region of the source is not copied: the module runs in the region the provider of the draft sets, or the one given in the values where it has a setting for it
- Not carried: name: the name of the resource on STACKIT is the customer's value, and nothing is copied from the source name
- Not carried: pubsub_config.topic: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: service_account: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add

### Proposed: one member's role on the Cloud Storage bucket

- Stands for: google_storage_bucket_iam_member.server, beside the draft of google_storage_bucket.media; that draft stays as it is and this is no step of it
- Module: `terraform/proposals/proposal_server`, switched on by `proposal_server_enabled`
- Not equal to the source: A STACKIT credentials group is not a grant on the Cloud Storage bucket. It is the principal that a bucket policy names, a set of S3 keys with no Google identity behind it, and by itself it gives no access to the bucket and takes none away. The statement that grants it something is a statement of the bucket policy, in the policy language of STACKIT (a credentials group or * as the principal, S3 actions, five condition keys), which no Terraform resource sets. The Google role is not mapped to S3 actions by any table, the condition of the grant has no counterpart unless it can be written with those five condition keys, and a Google group, a domain, a federated identity and allUsers or allAuthenticatedUsers are named again as credentials groups or as *.
- What you decide: Which credentials group stands for each member of the grant, which S3 actions of the bucket policy give the same people the access they need and no more than the Google role gave, what becomes of the condition, of public members (allUsers, allAuthenticatedUsers), of Google groups, domains and federated identities, and whether the grant ends with the Cloud Storage bucket when it has no replacement. Which bucket the grant is on, because the scan does not hold it.
- Not carried: member: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add
- Not carried: role: no value of the source is copied: the module asks the customer for the settings the pinned provider requires of its resources (name), and every other setting is the customer's to add

## Why some units are not carried by Terraform or the STACKIT API

16 units that Terraform and the STACKIT API do not carry are counted here once each, under one reason. Whatever else is open on a unit is in `execution-plan.json`.

- 3 units: STACKIT has a counterpart, but it does not do something these units rely on. `google_cloud_run_v2_job.migrate`, `google_cloud_run_v2_service.server`, `google_storage_bucket.media`
- 4 units: they need a different design on STACKIT, which an architect has to decide. `google_cloudbuild_trigger.activategcb`, `google_cloudbuild_trigger.init`, `google_cloudbuild_trigger.placeholder`, `google_firebase_hosting_site.client`
- 4 units: a decision of yours is open. `google_pubsub_topic.faux`, `google_secret_manager_secret.django_admin_password`, `google_secret_manager_secret.django_settings`, `google_sql_database_instance.postgres`
- 5 units: the assessment is not finished: a setting is still open for review, which says nothing yet about whether STACKIT can carry them. `google_firebase_project.default`, `google_service_account.automation`, `google_service_account.client`, `google_service_account.init`, `google_service_account.server`

## What cannot be migrated

- the critical setting `force-destroy` of `google_storage_bucket.media`: the target has no counterpart that carries the setting `force-destroy` of google_storage_bucket (the knowledge base marks it UNSUPPORTED). A check of the knowledge base (gcs force destroy) found a setting the target has no counterpart for (a finding of this unit describes it). stackit_objectstorage_bucket (pinned provider) "cannot be destroyed if the bucket contains objects" (schema description): force_destroy = false — a destroy that stops at a non-empty bucket — is what STACKIT does, PRESERVED; true has no counterpart, UNSUPPORTED (check gcs-force-destroy). Reason: the target has no counterpart that carries this setting of the source (`TARGET_SETTING_UNSUPPORTED`).
- google_firebase_project.default (google_firebase_project) (`google_firebase_project.default`): the target has no enabling of Firebase on a project. Nothing on STACKIT holds the registration of an app or the enabling of Firebase on a project. It stays with Google for as long as any Firebase service (push messaging, for example) is used, and a person decides, app by app, which backend replaces the Firebase services the app calls. Adding Firebase to a project cannot be undone. A redesign could List the Firebase services each app uses (authentication, database, storage, messaging, remote config); each needs its own replacement; The signing keys and store listings stay with Google Play and the Apple App Store; only the backend the app talks to moves. Reason: no STACKIT service or resource carries this source resource (`TARGET_RESOURCE_UNSUPPORTED`).

The draft writes no resource for 4 source resources: a person decides how each moves to STACKIT and sets its target up by hand (Manual steps):

- `google_firebase_hosting_site.client` — STACKIT has no web hosting service of this kind (0 hits for firebase in the resource names, descriptions and attributes of the 115 resource types of the pinned STACKIT provider, checked 2026-11-05; the product list at docs.stackit.cloud/products/ read 2026-11-05 names none), but it has the parts for static content: Object Storage buckets, and a CDN whose distribution can use a bucket as its origin and takes redirect rules, TLS settings and a WAF (the stackit_cdn_distribution resource of the pinned provider is a beta resource), with custom domains.
- `google_cloudbuild_trigger.activategcb` — STACKIT has no trigger or build resource.
- `google_cloudbuild_trigger.init` — STACKIT has no trigger or build resource.
- `google_cloudbuild_trigger.placeholder` — STACKIT has no trigger or build resource.

## Required decisions

5 decisions (9 questions) must be answered before the steps that wait for them can be carried out. Each is described under its id in `executive/decisions.md` and `decisions.json`.

- `DEC-GEN-CONVERT-API-OPT-IN` — 2 questions of this kind: `DEC-GEN-CONVERT-API-OPT-IN/google_pubsub_topic.faux` — Allow calls of the beta pubsub API v1beta for this migration (the API may change or vanish)? Accept: the pubsub API v1beta path is carried out for google_pubsub_topic.faux: its calls become executable. the RabbitMQ instance the draft writes stays in the draft this run wrote, so its step is held (REGENERATE_REQUIRED) until a draft is generated without it (answer target.messaging.path with pubsub in the inputs file and run again); an instance shared with a queue or topic that has no call of the pubsub API v1beta path stays taken. Refuse: the RabbitMQ instance the draft writes is carried out and the calls and steps of the pubsub API v1beta path are not taken.; `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password` — Allow calls of the Secrets Manager Vault-compatible API, whose maturity is not established, for this migration (the API may change or vanish)? Accept: the calls of the Secrets Manager Vault-compatible API (WriteSecret) are carried out for google_secret_manager_secret.django_admin_password, google_secret_manager_secret.django_settings. Refuse: no call is made, a refusal selects no other path (the draft holds no other carrier of what the calls set), and what they set stays open for google_secret_manager_secret.django_admin_password, google_secret_manager_secret.django_settings. — holds back 5 steps: the call `WriteSecret` of `secrets-manager-vault` (2 times); a manual step (3 times)
- `DEC-GEN-CONVERT-DATA-MOVEMENT` — Choose how the data of google_sql_database_instance.postgres moves from google_sql_database_instance to PostgreSQL Flex (stackit_postgresflex_instance.postgres): a dump restored inside the write freeze, the engine's logical replication, or a tool of your own — holds back 13 steps: a manual step (13 times)
- `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE` — 4 questions of this kind: `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.activategcb` — Decide whether the proposed STACKIT alternative replaces google_cloudbuild_trigger.activategcb: read the module proposals/proposal_activategcb, accept the difference and set its values, or design it another way; `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.init` — Decide whether the proposed STACKIT alternative replaces google_cloudbuild_trigger.init: read the module proposals/proposal_init, accept the difference and set its values, or design it another way; `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.placeholder` — Decide whether the proposed STACKIT alternative replaces google_cloudbuild_trigger.placeholder: read the module proposals/proposal_placeholder, accept the difference and set its values, or design it another way; `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_firebase_hosting_site.client` — Decide whether the proposed STACKIT alternative replaces google_firebase_hosting_site.client: read the module proposals/proposal_client, accept the difference and set its values, or design it another way — holds back 4 steps: a manual step (4 times)
- `DEC-GEN-CONVERT-SOURCE-PROJECT-MAPPING` — State where the units of each source project of the estate go — holds back 1 step: a manual step
- `DEC-GEN-CONVERT-SOURCE-REGION-MAPPING` — Map each source location of the estate to a STACKIT region — holds back 1 step: a manual step

To answer: write a decisions file (YAML or JSON, at most 1 MiB) with `schema_version` `0.2.0` and the head of `decisions.json` (`source_digest`, `kb_version` and `kb_sha256`), and under `decisions` copy the entry of each record you answer from the `template` of `decisions.json`. Fill in `resolution` (`RESOLVED_BY_ARCHITECT`, `ACCEPTED_DIFFERENCE`, `ACCEPTED_LOSS`, `ACCEPTED_REDESIGN`, or `REJECTED` when the proposal is not accepted: the decision then stays open), `reviewed_by` (the role that answers), `reviewed_at` (the date), `rationale`, `evidence_refs` (at least one reference for an answer that accepts) and, for a record that states options, `chosen_option`; keep `id`, `members`, `members_sha256` and `artifact_sha256` as the template gives them. Then run `stacktoit assess --decisions FILE` on the same source: an answer frees only the steps its effect allows, and a freed step says which answer freed it.

## Required credentials

- OpenTofu: the STACKIT provider reads its credentials from the environment, for example `STACKIT_SERVICE_ACCOUNT_KEY_PATH` (the path of a service account key file); the account needs the roles that create the resources of the steps. `terraform/README.md` says what the draft needs.
- OpenTofu variables a step needs a value for before it runs (a variable without a default, or one the plan does not take as given): `containers_machine_type`, `containers_nodes_max`, `containers_nodes_min`, `media_bucket_name`, `postgres_allowed_cidrs`, `postgres_storage_size`, `postgresflex_storage_class`, `project_id`, `rabbitmq_allowed_cidrs`, `rabbitmq_plan_name`, `rabbitmq_version`.
- OpenTofu variables the steps read with the default the draft declares (check each): `availability_zones`.
- API scripts: each reads its credentials and inputs from the environment when it runs, and stops when one is not set: `STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_ADMIN_PASSWORD`, `STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_SETTINGS`, `STACKIT_INPUT_SECRET_NAME_DJANGO_ADMIN_PASSWORD`, `STACKIT_INPUT_SECRET_NAME_DJANGO_SETTINGS`. No script holds a secret. `STACKIT_INPUT_INSTANCEID`, `STACKIT_INPUT_PASSWORD`, `STACKIT_INPUT_USERNAME`: the override of a value the draft writes as an output; when it is not set and tofu is installed, the script reads it with `tofu -chdir=terraform output -raw <output>` once the draft is applied (the request file lists the output of each). `STACKIT_API_URL_SECRETS_MANAGER_VAULT` may name another base address; without it the script uses the documented address of the service for the region of the draft.

## Execution order

The steps in the order they are carried out: a step comes after every step it depends on.

### Prepare

1. The step "Decide how google_cloud_run_v2_job.migrate gets its application identity on…" (`MAN/identity/facts:google_cloud_run_v2_job.migrate:google_cloud_run_v2_job.migrate.template.0.template.0.service_account-8dc3e1`) — manual step: …STACKIT (a service account of its own, workload federation): the draft does not write it. (executable: a person can start it now)
2. The step "Decide how google_cloud_run_v2_service.server gets its application identity on…" (`MAN/identity/facts:google_cloud_run_v2_service.server:google_cloud_run_v2_service.server.template.0.service_account-5ccce9`) — manual step: …STACKIT (a service account of its own, workload federation): the draft does not write it. (executable: a person can start it now)
3. The step "Decide who may use the STACKIT service account written for…" for google_service_account.automation (`MAN/identity/held:principal:google_service_account.automation`) — manual step: …google_service_account.automation, what it may do and which workloads run as it: the draft attaches it to nothing. (executable: a person can start it now)
4. The step "Decide who may use the STACKIT service account written for…" for google_service_account.server (`MAN/identity/held:principal:google_service_account.server`) — manual step: …google_service_account.server, what it may do and which workloads run as it: the draft attaches it to nothing. (executable: a person can start it now)

### Security

5. The review of the open settings of google_secret_manager_secret.django_admin_password (`MAN/semantic/google_secret_manager_secret.django_admin_password/open-settings-security-architect`) — manual step: Review the 2 open critical settings of `google_secret_manager_secret.django_admin_password` (google_secret_manager_secret) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`secret-value`, `tcr-secret-id`). (executable: a person can start it now)
6. The setting `secret-settings` of google_secret_manager_secret.django_admin_password (`MAN/semantic/google_secret_manager_secret.django_admin_password/secret-settings`) — manual step: Decide what the STACKIT target of `google_secret_manager_secret.django_admin_password` (google_secret_manager_secret) does for its critical setting `secret-settings`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
7. The review of the open settings of google_secret_manager_secret.django_settings (`MAN/semantic/google_secret_manager_secret.django_settings/open-settings-security-architect`) — manual step: Review the 2 open critical settings of `google_secret_manager_secret.django_settings` (google_secret_manager_secret) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`secret-value`, `tcr-secret-id`). (executable: a person can start it now)
8. The setting `secret-settings` of google_secret_manager_secret.django_settings (`MAN/semantic/google_secret_manager_secret.django_settings/secret-settings`) — manual step: Decide what the STACKIT target of `google_secret_manager_secret.django_settings` (google_secret_manager_secret) does for its critical setting `secret-settings`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
9. The review of the open settings of google_service_account.automation (`MAN/semantic/google_service_account.automation/open-settings`) — manual step: Review the 2 open critical settings of `google_service_account.automation` (google_service_account) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`role-bindings`, `account-name`). (executable: a person can start it now)
10. The review of the open settings of google_service_account.client (`MAN/semantic/google_service_account.client/open-settings`) — manual step: Review the 2 open critical settings of `google_service_account.client` (google_service_account) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`role-bindings`, `account-name`). (executable: a person can start it now)
11. The review of the open settings of google_service_account.init (`MAN/semantic/google_service_account.init/open-settings`) — manual step: Review the 2 open critical settings of `google_service_account.init` (google_service_account) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`role-bindings`, `account-name`). (executable: a person can start it now)
12. The review of the open settings of google_service_account.server (`MAN/semantic/google_service_account.server/open-settings`) — manual step: Review the 2 open critical settings of `google_service_account.server` (google_service_account) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`role-bindings`, `account-name`). (executable: a person can start it now)
13. The OpenTofu step for stackit_secretsmanager_instance.migrated_secrets (`TF/stackit_secretsmanager_instance.migrated_secrets`) — OpenTofu: creates `stackit_secretsmanager_instance.migrated_secrets` (`terraform/security.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))
14. The OpenTofu step for stackit_secretsmanager_user.migration (`TF/stackit_secretsmanager_user.migration`) — OpenTofu: creates `stackit_secretsmanager_user.migration` (`terraform/security.tf`). After step 13. (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))
15. The STACKIT API step WriteSecret of google_secret_manager_secret.django_admin_password (`API/secrets-manager-vault/WriteSecret/google_secret_manager_secret.django_admin_password`) — STACKIT API call: `secrets-manager-vault` `WriteSecret` (`execution/api/secrets-manager-vault/001-writesecret.sh`). Step state: generated, tested offline. After steps 13, 14. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; and, as the plan states: Required input: STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_ADMIN_PASSWORD names a file (keep it outside the package and out of version control) that holds the JSON object of the key/value pairs the secret at the path STACKIT_INPUT_SECRET_NAME_DJANGO_ADMIN_PASSWORD names has on STACKIT. StackToIT never reads the value of google_secret_manager_secret.django_admin_password from the source, so the document is yours: the same value from a secure source, or a new one. The source builds the value (its version resource, attribute secret_data) from a value it generates itself (random_password.django_admin_password): the document holds the matching values on STACKIT (for a generated value, one you generate or choose for STACKIT: the generated one is only in the source's state), under the keys the consumers of the secret read (the key names are in the source's secret_data, which the scan does not read); To make the document from Google Cloud, if the value is still there, run this yourself with your own Google credentials (StackToIT never runs it): `gcloud secrets versions access latest --secret=\<the Google secret id> --project=\<the Google project> --out-file=\<a file outside the package>`, put its content under the key the applications read it under in a JSON object (`{"\<key>": "\<the content>"}`) in the file STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_ADMIN_PASSWORD names, and delete both files after the write; The path of the secret is an input: STACKIT_INPUT_SECRET_NAME_DJANGO_ADMIN_PASSWORD names it (the source does not state the name as a literal the plan can rely on), so this step does not carry the setting `name` and its manual step stays; the applications read the secret by the path you choose; The step logs in as the migration user stackit_secretsmanager_user.migration (write_enabled = true), which reaches every secret of stackit_secretsmanager_instance.migrated_secrets and whose generated password is in the OpenTofu state (a sensitive attribute): delete the user (remove its block from the draft and apply) once the secrets are written. The script reads the username and password with tofu output, or from STACKIT_INPUT_USERNAME and STACKIT_INPUT_PASSWORD, and prints neither; The write makes a new version of the secret. The script reads the secret first (ReadSecret) and stops without writing (exit 4) when it already has a current version, unless STACKIT_CONFIRM_REPLACE is set to the path of the secret; it does not read the values back and prints none; STACKIT's page shows neither the answer of the login nor the status of a read of a secret that has no version: the script takes the token from auth.client_token and reads 404 as "no current version" (the Vault KV version 2 API the page calls compatible), and stops on any other answer. The API states no release status, so the call waits for the opt-in to it; the maturity of the Secrets Manager Vault-compatible API is not established (its documentation states no release status): a call of it is not executed without an explicit opt-in (DEC-GEN-CONVERT-API-OPT-IN); the migration of `google_secret_manager_secret.django_admin_password` is not complete with this step: 3 critical settings of it that the draft does not carry are open in 2 steps, a step standing for several of them (3 manual, 0 refused): `secret-value`, `secret-settings`, `tcr-secret-id`)
16. The STACKIT API step WriteSecret of google_secret_manager_secret.django_settings (`API/secrets-manager-vault/WriteSecret/google_secret_manager_secret.django_settings`) — STACKIT API call: `secrets-manager-vault` `WriteSecret` (`execution/api/secrets-manager-vault/002-writesecret.sh`). Step state: generated, tested offline. After steps 13, 14. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; and, as the plan states: Required input: STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_SETTINGS names a file (keep it outside the package and out of version control) that holds the JSON object of the key/value pairs the secret at the path STACKIT_INPUT_SECRET_NAME_DJANGO_SETTINGS names has on STACKIT. StackToIT never reads the value of google_secret_manager_secret.django_settings from the source, so the document is yours: the same value from a secure source, or a new one. The source builds the value (its version resource, attribute secret_data) from google_sql_database.database, google_sql_database_instance.postgres, google_sql_user.django, google_storage_bucket.media and a value it generates itself (random_password.django_secret_key): the document holds the matching values on STACKIT (for a generated value, one you generate or choose for STACKIT: the generated one is only in the source's state), under the keys the consumers of the secret read (the key names are in the source's secret_data, which the scan does not read); To make the document from Google Cloud, if the value is still there, run this yourself with your own Google credentials (StackToIT never runs it): `gcloud secrets versions access latest --secret=\<the Google secret id> --project=\<the Google project> --out-file=\<a file outside the package>`, put its content under the key the applications read it under in a JSON object (`{"\<key>": "\<the content>"}`) in the file STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_SETTINGS names, and delete both files after the write; The path of the secret is an input: STACKIT_INPUT_SECRET_NAME_DJANGO_SETTINGS names it (the source does not state the name as a literal the plan can rely on), so this step does not carry the setting `name` and its manual step stays; the applications read the secret by the path you choose; The step logs in as the migration user stackit_secretsmanager_user.migration (write_enabled = true), which reaches every secret of stackit_secretsmanager_instance.migrated_secrets and whose generated password is in the OpenTofu state (a sensitive attribute): delete the user (remove its block from the draft and apply) once the secrets are written. The script reads the username and password with tofu output, or from STACKIT_INPUT_USERNAME and STACKIT_INPUT_PASSWORD, and prints neither; The write makes a new version of the secret. The script reads the secret first (ReadSecret) and stops without writing (exit 4) when it already has a current version, unless STACKIT_CONFIRM_REPLACE is set to the path of the secret; it does not read the values back and prints none; STACKIT's page shows neither the answer of the login nor the status of a read of a secret that has no version: the script takes the token from auth.client_token and reads 404 as "no current version" (the Vault KV version 2 API the page calls compatible), and stops on any other answer. The API states no release status, so the call waits for the opt-in to it; the maturity of the Secrets Manager Vault-compatible API is not established (its documentation states no release status): a call of it is not executed without an explicit opt-in (DEC-GEN-CONVERT-API-OPT-IN); the migration of `google_secret_manager_secret.django_settings` is not complete with this step: 3 critical settings of it that the draft does not carry are open in 2 steps, a step standing for several of them (3 manual, 0 refused): `secret-value`, `secret-settings`, `tcr-secret-id`)
17. The step "Read each migrated secret" (`MAN/path/secrets-migration/read-back`) — manual step: (google_secret_manager_secret.django_admin_password and google_secret_manager_secret.django_settings) back from the Secrets Manager instance stackit_secretsmanager_instance.migrated_secrets before the migration user is removed: with its login, send the ReadSecret request of the write step (GET /v1/\<instance id>/data/\<path>, the path the write step names) and check that the secret has a current version, that its keys are the keys of the document you supplied, and that its values equal the document's (compare a hash of each value on your side; print none). After steps 15, 16. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; and, as the plan states: a value printed while comparing is a leaked secret, so compare hashes and never the values themselves)
18. The step "Remove the migration user stackit_secretsmanager_user.migration of the Secrets…" (`MAN/path/secrets-migration/retire-user`) — manual step: …Manager instance stackit_secretsmanager_instance.migrated_secrets once the last secret is written and read back: delete its block and its two outputs (stackit_secretsmanager_user_migration_password and stackit_secretsmanager_user_migration_username) from the draft and run tofu apply, so the provider deletes the user. Afterwards the state must not list it (tofu state list shows no stackit_secretsmanager_user.migration) and the instance must list no user of that name; give a workload that reads a secret a user of its own with write_enabled = false, or the instance's role binding. After step 17. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; it also comes after step 17, a manual prerequisite; and, as the plan states: until this step is done a credential with write access to every secret of the instance exists and its password is in the state)
19. The OpenTofu step for stackit_service_account.automation (`TF/stackit_service_account.automation`) — OpenTofu: creates `stackit_service_account.automation` (`terraform/identity.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))
20. The OpenTofu step for stackit_service_account.client (`TF/stackit_service_account.client`) — OpenTofu: creates `stackit_service_account.client` (`terraform/identity.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))
21. The OpenTofu step for stackit_service_account.init (`TF/stackit_service_account.init`) — OpenTofu: creates `stackit_service_account.init` (`terraform/identity.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))
22. The OpenTofu step for stackit_service_account.server (`TF/stackit_service_account.server`) — OpenTofu: creates `stackit_service_account.server` (`terraform/identity.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id))

### Data services

23. The review of the open settings of google_sql_database_instance.postgres "SOURCE_PROJECT_MAPPING" (`MAN/estate/SOURCE_PROJECT_MAPPING`) — manual step: Answer the estate's question once for 1 unit: it holds 1 open critical setting, and no unit has a step of its own for it. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-SOURCE-PROJECT-MAPPING`; and, as the plan states: the migration is not complete until the 1 setting of this step are closed)
24. The review of the open settings of google_cloud_run_v2_job.migrate, google_cloud_run_v2_service.server, google_sql_database_instance.postgres and 1 more (`MAN/estate/SOURCE_REGION_MAPPING`) — manual step: Answer the estate's question once for 4 units: it holds 4 open critical settings, and no unit has a step of its own for it. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-SOURCE-REGION-MAPPING`; and, as the plan states: the migration is not complete until the 4 settings of this step are closed)
25. The setting `database-users` of google_sql_database_instance.postgres (`MAN/semantic/google_sql_database_instance.postgres/database-users`) — manual step: Decide what the STACKIT target of `google_sql_database_instance.postgres` (google_sql_database_instance) does for its critical setting `database-users`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
26. The review of the open settings of google_sql_database_instance.postgres "open-settings-platform-architect" (`MAN/semantic/google_sql_database_instance.postgres/open-settings-platform-architect`) — manual step: Review the 2 open critical settings of `google_sql_database_instance.postgres` (google_sql_database_instance) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`tc-name`, `tcf-sql-database--name`). (executable: a person can start it now)
27. The setting `user-and-database-settings` of google_sql_database_instance.postgres (`MAN/semantic/google_sql_database_instance.postgres/user-and-database-settings`) — manual step: Decide what the STACKIT target of `google_sql_database_instance.postgres` (google_sql_database_instance) does for its critical setting `user-and-database-settings`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
28. The setting `geo-replication` of google_storage_bucket.media (`MAN/semantic/google_storage_bucket.media/geo-replication`) — manual step: Decide what the STACKIT target of `google_storage_bucket.media` (google_storage_bucket) does for its critical setting `geo-replication`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
29. The setting `iam-access` of google_storage_bucket.media (`MAN/semantic/google_storage_bucket.media/iam-access`) — manual step: Decide what the STACKIT target of `google_storage_bucket.media` (google_storage_bucket) does for its critical setting `iam-access`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
30. The review of the open settings of google_storage_bucket.media (`MAN/semantic/google_storage_bucket.media/open-settings-identity-architect`) — manual step: Review the 2 open critical settings of `google_storage_bucket.media` (google_storage_bucket) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`iam-grant`, `public-access`). (executable: a person can start it now)
31. The setting "name" of google_storage_bucket.media (`MAN/semantic/google_storage_bucket.media/tc-storage-bucket-name`) — manual step: Decide what the STACKIT target of `google_storage_bucket.media` (google_storage_bucket) does for its critical setting `tc-storage-bucket-name`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
32. The proposed alternative of google_firebase_hosting_site.client (`MAN/unit/google_firebase_hosting_site.client`) — manual step: Decide whether the STACKIT alternative in the switched-off module terraform/proposals/proposal_client replaces google_firebase_hosting_site.client (google_firebase_hosting_site): read the module, accept the stated difference, give it its values and set proposal_client_enabled to true, or set the target up another way by hand. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_firebase_hosting_site.client`; and, as the plan states: the assessment says: A Firebase Hosting site: the unit of hosting, whose identifier builds the Firebase-provided sub-domains, optionally associated with a Firebase web app. Firebase Hosting is Google's web hosting with a global CDN: a site holds versions (the files with the rewrites, redirects and headers of firebase.json), releases publish a version on a channel, preview channels expire by themselves, and custom domains get a certificate. STACKIT has no web hosting service of this kind (0 hits for firebase in the resource names, descriptions and attributes of the 115 resource types of the pinned STACKIT provider, checked 2026-11-05; the product list at docs.stackit.cloud/products/ read 2026-11-05 names none), but it has the parts for static content: Object Storage buckets, and a CDN whose distribution can use a bucket as its origin and takes redirect rules, TLS settings and a WAF (the stackit_cdn_distribution resource of the pinned provider is a beta resource), with custom domains. Rewrites to Cloud Run services or Cloud Functions, preview channels and the release history of a site have no counterpart there. Static files are published into an Object Storage bucket by a pipeline; a CDN distribution with the bucket as origin serves them on a custom domain, and the redirects of firebase.json become redirect rules of the distribution. Preview channels and release history are rebuilt in the pipeline (one bucket path per preview). Rewrites to dynamic backends need a load balancer or another origin, which a person decides; the assessment classifies google_firebase_hosting_site.client as redesign: only its switched-off module holds a target, and that is a proposal; 2 critical settings of `google_firebase_hosting_site.client` are open and part of this step: `tc-firebase-hosting-site-project`, `tcr-firebase-hosting-site-site-id` (`assurance/critical-semantics.json` says why each is open and what closes it); google_firebase_hosting_site.client exists only under a condition of the source (applies only if its count (var.random_suffix ? 1 : 0) holds when OpenTofu plans — present with the defaults of var.random_suffix (1 instance); a variable file (-var-file), -var or TF_VAR_random_suffix can change that))
33. The refusal of the setting `force-destroy` of google_storage_bucket.media (`REF/semantic/google_storage_bucket.media/force-destroy`) — refused: not migrated: the critical setting `force-destroy` of `google_storage_bucket.media`. (permanently unsupported: refused, not migrated (not executable))
34. The OpenTofu step for stackit_objectstorage_bucket.media (`TF/stackit_objectstorage_bucket.media`) — OpenTofu: creates `stackit_objectstorage_bucket.media` (`terraform/storage.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id, media_bucket_name))
35. The data preflight of google_storage_bucket.media (`MAN/data/google_storage_bucket.media/preflight`) — manual step: Close what the plan lists as UNKNOWN, from the move host, before any object moves. The credentials of the target are the environment names TARGET_ACCESS_KEY_ID and TARGET_SECRET_ACCESS_KEY, set in the shell of the move host at run time from the credential of a credentials group; the source uses the Google identity of that shell (the service account the host runs as, or a login you made for the move; rclone's env_auth takes it from the environment). No value is written into a file, a ticket or this plan. In the shell of the move host configure the two remotes from the environment: `export RCLONE_CONFIG_SRC_TYPE="google cloud storage" RCLONE_CONFIG_SRC_ENV_AUTH=true RCLONE_CONFIG_DST_TYPE=s3 RCLONE_CONFIG_DST_PROVIDER=Other RCLONE_CONFIG_DST_ENDPOINT=https://object.storage.eu01.onstackit.cloud RCLONE_CONFIG_DST_ACCESS_KEY_ID="$TARGET_ACCESS_KEY_ID" RCLONE_CONFIG_DST_SECRET_ACCESS_KEY="$TARGET_SECRET_ACCESS_KEY" RCLONE_CONFIG_DST_ACL=private`. Source: `rclone lsd SRC:`, `rclone size SRC:\<source-bucket> --json` (the count and the bytes), `gcloud storage buckets describe gs://\<source-bucket>` (versioning, retention policy, default key, requester pays, soft delete policy, storage class), and for the objects the owner names `rclone lsjson --metadata SRC:\<source-bucket> --files-only -R --max-depth 1`. Target (after the draft is applied, the bucket exists and a key of a credentials group is issued): `rclone lsd DST:` and `rclone size DST:\<target-bucket> --json`: the bucket is empty. Record the number of objects and the bytes beside the size the customer stated (the size of the data is not provided); the plan has no input for the number of objects. Check that the move host reaches both endpoints. Runbook section: `terraform/handover/bucket-media/data-copy.md#preflight`. (`terraform/handover/bucket-media/data-copy.md`) After step 34. (executable once the automated step it comes after has run: it waits for step 34)
36. The data rehearsal of google_storage_bucket.media (`MAN/data/google_storage_bucket.media/rehearsal`) — manual step: Rehearse the copy with a representative part of the bucket: the objects of one prefix and the largest and the smallest objects, into the prefix `stacktoit-rehearsal/` of the target bucket. Note the elapsed time and the bytes a second for small and for large objects, compare `rclone lsjson --metadata` of a sample on both sides (content type, custom metadata, storage class, modification time), and delete the prefix `stacktoit-rehearsal/` when the rehearsal is done. Runbook section: `terraform/handover/bucket-media/data-copy.md#rehearsal`. After step 35. (executable once its manual prerequisite is done: it waits for step 35, a manual prerequisite)
37. The initial copy of the data of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/initial-copy/rclone-copy`) — manual step: While the writers keep writing to the source, copy every live object to the target. The credentials of the target are the environment names TARGET_ACCESS_KEY_ID and TARGET_SECRET_ACCESS_KEY, set in the shell of the move host at run time from the credential of a credentials group; the source uses the Google identity of that shell (the service account the host runs as, or a login you made for the move; rclone's env_auth takes it from the environment). No value is written into a file, a ticket or this plan. With the remotes of the preflight: `rclone copy SRC:\<source-bucket> DST:\<target-bucket> --s3-upload-cutoff 0 --progress --log-file=\<log-file>` (the form STACKIT's tutorial uses). Read the log for every error before the next step. Runbook section: `terraform/handover/bucket-media/data-copy.md#initial-copy-rclone-copy`. After step 36. (executable once its manual prerequisite is done: it waits for step 36, a manual prerequisite)
38. The replication watch of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/delta-sync/rclone-copy`) — manual step: Run the same `rclone copy` again, as often as the business needs the target to be close to the source: each pass skips the objects that are identical (rclone compares size and modification time; `--checksum` compares size and hash where there is one, and Cloud Storage stores the MD5 natively) and copies the ones that are new or changed. It deletes nothing on the target, so an object deleted in the source stays there until the final pass. Note how many objects and bytes each pass transferred. Runbook section: `terraform/handover/bucket-media/data-copy.md#delta-sync-rclone-copy`. After step 37. (executable once its manual prerequisite is done: it waits for step 37, a manual prerequisite)
39. The OpenTofu step for stackit_postgresflex_instance.postgres (`TF/stackit_postgresflex_instance.postgres`) — OpenTofu: creates `stackit_postgresflex_instance.postgres` (`terraform/data.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id, postgres_allowed_cidrs, postgres_storage_size, postgresflex_storage_class))
40. The data preflight of google_sql_database_instance.postgres (`MAN/data/google_sql_database_instance.postgres/preflight`) — manual step: Close what the plan lists as UNKNOWN, from the move host, with read-only queries. The move host reaches the source through the connection method you already use for Cloud SQL for PostgreSQL (an authorized network of a public address, a private address inside the VPC network, or a proxy you run). Source: `PGPASSWORD="$SOURCE_PGPASSWORD" PGSSLMODE=require psql --host=\<source-host> --port=\<source-port> --username=\<source-user> --dbname=\<source-db> --command='SHOW server_version;'`, `SELECT extname, extversion FROM pg_extension ORDER BY 1;`, `SELECT pg_size_pretty(pg_database_size(current_database));` against the free storage of the instance (at least 25 percent of the database size free before a large export), `\du` and `\dp` for the roles and grants, and the users that sign in with an IAM identity. Target (after the draft is applied and the instance, user and database exist): `PGPASSWORD="$TARGET_PGPASSWORD" PGSSLMODE=require psql --host=\<target-host> --port=\<target-port> --username=\<target-user> --dbname=\<target-db> --command='SHOW extwlist.extensions;'` and `SELECT version;`. Check the move host is in `network.acl` of the instance, reaches both ends and speaks TLS 1.3. Logical replication: on the source `SHOW wal_level;`, `SHOW max_replication_slots;`, `SHOW max_wal_senders;` and the tables without a primary key (`SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema') AND NOT EXISTS (SELECT 1 FROM pg_index i WHERE i.indrelid = c.oid AND i.indisprimary);`); on the empty target the probe `CREATE SUBSCRIPTION stacktoit_probe CONNECTION 'host=localhost dbname=x' PUBLICATION p WITH (connect = false);` then `ALTER SUBSCRIPTION stacktoit_probe SET (slot_name = NONE);` and `DROP SUBSCRIPTION stacktoit_probe;`. Runbook section: `terraform/handover/database-postgres/data-move.md#preflight`. (`terraform/handover/database-postgres/data-move.md`) After step 39. (executable once the automated step it comes after has run: it waits for step 39)
41. The data rehearsal of google_sql_database_instance.postgres (`MAN/data/google_sql_database_instance.postgres/rehearsal`) — manual step: Rehearse the chosen method into a scratch database of the target (a second `stackit_postgresflex_database` or a clone of the instance) from a copy of the source (a clone or a restored backup of the Cloud SQL for PostgreSQL instance, so that production is not loaded). Note the elapsed time of the dump, the restore, the index build and the verification. Drop the scratch database and the copy afterwards. Runbook section: `terraform/handover/database-postgres/data-move.md#rehearsal`. After step 40. (executable once its manual prerequisite is done: it waits for step 40, a manual prerequisite)
42. The initial copy of the data of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/initial-copy/logical-replication`) — manual step: Source: turn logical decoding on: set the flag `cloudsql.logical_decoding` to `on` on the instance (a restart of the instance), create a user for the publication with the REPLICATION attribute that holds CREATE on the database (`CREATE ROLE stacktoit_repl WITH LOGIN REPLICATION PASSWORD '\<replication-password>';`, typed in an interactive session). Every table needs a primary key or a replica identity. Copy the schema (no later DDL on either side): `PGPASSWORD="$SOURCE_PGPASSWORD" pg_dump --schema-only --no-owner --no-acl --host=\<source-host> --port=\<source-port> --username=\<source-user> --dbname=\<source-db> | PGPASSWORD="$TARGET_PGPASSWORD" PGSSLMODE=require psql --host=\<target-host> --port=\<target-port> --username=\<target-user> --dbname=\<target-db>`. Publish on the source: `CREATE PUBLICATION stacktoit_move FOR ALL TABLES;`. Subscribe on the target in an interactive psql session so that no password is stored in a file or the shell history: `CREATE SUBSCRIPTION stacktoit_move CONNECTION 'host=\<source-host> port=\<source-port> dbname=\<source-db> user=stacktoit_repl password=\<replication-password> sslmode=require' PUBLICATION stacktoit_move;`. Wait until `SELECT srrelid::regclass, srsubstate FROM pg_subscription_rel;` shows `r` for every table. Runbook section: `terraform/handover/database-postgres/data-move.md#initial-copy-logical-replication`. After step 41. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 41, a manual prerequisite; and, as the plan states: a change and a restart of the source; a slot nobody reads keeps WAL on the disk of the source; schema changes freeze from here on; this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default)
43. The replication watch of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/delta-sync/logical-replication`) — manual step: Keep the target apart: the application does not write to it and no DDL runs on either side. Watch the lag and the free storage of the source: `SELECT slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), confirmed_flush_lsn)) AS lag FROM pg_replication_slots;`. Runbook section: `terraform/handover/database-postgres/data-move.md#delta-sync-logical-replication`. After step 42. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 42, a manual prerequisite; and, as the plan states: a growing lag means the copy does not catch up at this write rate; this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default)

### Platform services

44. The step "Create the topic under a displayName of 1 to 36 letters, digits, spaces and…" (`MAN/pubsub.topic/name/google_pubsub_topic.faux`) — manual step: …hyphens and change its publishers to it, or keep the topic on the RabbitMQ path. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_pubsub_topic.faux`; and, as the plan states: this step holds only once DEC-GEN-CONVERT-API-OPT-IN is answered: it is of the candidate path pubsub.topic of google_pubsub_topic.faux, not of the path the draft carries out by default)
45. The step "Declare the topics and subscriptions of the source on the RabbitMQ instance" (`MAN/rabbitmq/topology`) — manual step: (exchanges: 1, queues: 0): run rabbitmq/apply-topology.sh, which writes rabbitmq/topology.json through the HTTP API of the broker. Nothing of this was run on STACKIT, and whether the credential may declare them is not stated: the script asks first and stops with the answer. (executable: a person can start it now)
46. The setting `cloud-run-v2-job-network-exposure` of google_cloud_run_v2_job.migrate (`MAN/semantic/google_cloud_run_v2_job.migrate/cloud-run-v2-job-network-exposure`) — manual step: Decide what the STACKIT target of `google_cloud_run_v2_job.migrate` (google_cloud_run_v2_job) does for its critical setting `cloud-run-v2-job-network-exposure`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
47. The review of the open settings of google_cloud_run_v2_job.migrate (`MAN/semantic/google_cloud_run_v2_job.migrate/open-settings-platform-architect`) — manual step: Review the 6 open critical settings of `google_cloud_run_v2_job.migrate` (google_cloud_run_v2_job) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`k4-cloud-run-volume-mount-cloud-run-v2-job`, `terraform-destroy-guard`, `tc-cloud-run-v2-job-name`, `tc-cloud-run-v2-job-template--template--volumes--name`, `deployment-artifact`, `entry-point`). (executable: a person can start it now)
48. The setting `secrets` of google_cloud_run_v2_job.migrate (`MAN/semantic/google_cloud_run_v2_job.migrate/secrets`) — manual step: Decide what the STACKIT target of `google_cloud_run_v2_job.migrate` (google_cloud_run_v2_job) does for its critical setting `secrets`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
49. The setting `service-identity` of google_cloud_run_v2_job.migrate (`MAN/semantic/google_cloud_run_v2_job.migrate/service-identity`) — manual step: Decide what the STACKIT target of `google_cloud_run_v2_job.migrate` (google_cloud_run_v2_job) does for its critical setting `service-identity`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
50. The setting `cloud-sql-connection` of google_cloud_run_v2_service.server (`MAN/semantic/google_cloud_run_v2_service.server/cloud-sql-connection`) — manual step: Decide what the STACKIT target of `google_cloud_run_v2_service.server` (google_cloud_run_v2_service) does for its critical setting `cloud-sql-connection`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
51. The review of the open settings of google_cloud_run_v2_service.server by the Identity Architect (`MAN/semantic/google_cloud_run_v2_service.server/open-settings-identity-architect`) — manual step: Review the 2 open critical settings of `google_cloud_run_v2_service.server` (google_cloud_run_v2_service) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`iam-invocation`, `service-identity`). (executable: a person can start it now)
52. The review of the open settings of google_cloud_run_v2_service.server by the Platform Architect (`MAN/semantic/google_cloud_run_v2_service.server/open-settings-platform-architect`) — manual step: Review the 6 open critical settings of `google_cloud_run_v2_service.server` (google_cloud_run_v2_service) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`k4-cloud-run-volume-mount-cloud-run-v2-service`, `iam-policy-grant`, `terraform-destroy-guard`, `tc-cloud-run-v2-service-name`, `tc-cloud-run-v2-service-template--volumes--name`, `deployment-artifact`). (executable: a person can start it now)
53. The review of the open settings of google_cloud_run_v2_service.server by the Security Architect (`MAN/semantic/google_cloud_run_v2_service.server/open-settings-security-architect`) — manual step: Review the 2 open critical settings of `google_cloud_run_v2_service.server` (google_cloud_run_v2_service) that the draft does not carry, and decide for each what the STACKIT target does: set it up by hand, or record that production does not need it (`environment`, `secrets`). (executable: a person can start it now)
54. The setting "name" of google_pubsub_topic.faux (`MAN/semantic/google_pubsub_topic.faux/tc-name`) — manual step: Decide what the STACKIT target of `google_pubsub_topic.faux` (google_pubsub_topic) does for its critical setting `tc-name`, which the draft does not carry, and set it up by hand, or record that production does not need it. (executable: a person can start it now)
55. The refusal of google_firebase_project.default (`REF/unit/google_firebase_project.default`) — refused: not migrated: google_firebase_project.default (google_firebase_project). (permanently unsupported: refused, not migrated (not executable))
56. The OpenTofu step for stackit_rabbitmq_instance.messaging (`TF/stackit_rabbitmq_instance.messaging`) — OpenTofu: creates `stackit_rabbitmq_instance.messaging` (`terraform/messaging.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id, rabbitmq_allowed_cidrs, rabbitmq_plan_name, rabbitmq_version))
57. The OpenTofu step for stackit_ske_cluster.containers (`TF/stackit_ske_cluster.containers`) — OpenTofu: creates `stackit_ske_cluster.containers` (`terraform/kubernetes.tf`). (executable once a value is supplied: it is free of decisions and manual prerequisites and waits for a value (project_id, containers_machine_type, containers_nodes_max, containers_nodes_min))
58. The step "Fetch the kubeconfig, check the health, create the namespace and the secrets of…" (`MAN/draft/handover/cluster-access/kubeconfig-md`) — manual step: …the SKE cluster(s) of the draft: the namespace is one you choose for all manifest files, the secrets are migrate-secrets, server-secrets (the keys of each are listed under SECRETS TO CREATE in the header of its manifest file). (`terraform/handover/cluster-access/kubeconfig.md`) After step 57. (executable once the automated step it comes after has run: it waits for step 57)

### Workloads

59. The step "Fill in the required inputs of kubernetes/migrate/manifests.yaml and apply it…" (`MAN/draft/kubernetes/migrate/manifests-yaml`) — manual step: …to the cluster stackit_ske_cluster.containers: kubectl apply -n \<namespace> -f \<manifest file>; the Job is written suspended and runs when you unsuspend it. (`terraform/kubernetes/migrate/manifests.yaml`) After steps 57, 58. (executable once its manual prerequisite is done: it waits for step 58, a manual prerequisite)
60. The step "Fill in the required inputs of kubernetes/server/manifests.yaml and apply it to…" (`MAN/draft/kubernetes/server/manifests-yaml`) — manual step: …the cluster stackit_ske_cluster.containers (put 0 in the number of pods): kubectl apply -n \<namespace> -f \<manifest file>; no pod starts. The pods start when the data they connect to is moved, by the person who runs the cutover (step 2 of the header). (`terraform/kubernetes/server/manifests.yaml`) After steps 57, 58. (executable once its manual prerequisite is done: it waits for step 58, a manual prerequisite)
61. The proposed alternative of google_cloudbuild_trigger.activategcb (`MAN/unit/google_cloudbuild_trigger.activategcb`) — manual step: Decide whether the STACKIT alternative in the switched-off module terraform/proposals/proposal_activategcb replaces google_cloudbuild_trigger.activategcb (google_cloudbuild_trigger): read the module, accept the stated difference, give it its values and set proposal_activategcb_enabled to true, or set the target up another way by hand. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.activategcb`; and, as the plan states: the assessment says: A Cloud Build trigger starts a build when a source event happens (a push or a pull request on GitHub, Bitbucket Server, a repository of the 2nd generation, Developer Connect or Cloud Source Repositories), when a Pub/Sub message arrives, when a webhook is called or when a person runs it. The build is a list of container steps (a build block, or a file in the repository) with substitutions, images and artifacts to push, secrets from Secret Manager, a service account, a log destination and optionally a manual approval. STACKIT has no trigger or build resource. The same delivery is a workflow of STACKIT Git Pipelines (workflows written for GitHub Actions; STACKIT-managed or custom runners; secrets from STACKIT Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Nothing is converted: the trigger conditions (branch and tag expressions, file filters, the event kind), the steps (a container step becomes a step or a job with a container), the secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person. The pages read describe pipelines that run when changes are pushed; other events (pull requests, schedules, manual runs) are verified against the Forgejo Actions reference; the assessment classifies google_cloudbuild_trigger.activategcb as redesign: only its switched-off module holds a target, and that is a proposal; 6 critical settings of `google_cloudbuild_trigger.activategcb` are open and part of this step: `project-residency`, `build-identity`, `log-destination`, `deployment-artifact`, `entry-point`, `dep-references` (`assurance/critical-semantics.json` says why each is open and what closes it); google_cloudbuild_trigger.activategcb exists only under a condition of the source (applies only if its count (var.init ? 1 : 0) holds when OpenTofu plans — present with the defaults of var.init (1 instance); a variable file (-var-file), -var or TF_VAR_init can change that))
62. The proposed alternative of google_cloudbuild_trigger.init (`MAN/unit/google_cloudbuild_trigger.init`) — manual step: Decide whether the STACKIT alternative in the switched-off module terraform/proposals/proposal_init replaces google_cloudbuild_trigger.init (google_cloudbuild_trigger): read the module, accept the stated difference, give it its values and set proposal_init_enabled to true, or set the target up another way by hand. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.init`; and, as the plan states: the assessment says: A Cloud Build trigger starts a build when a source event happens (a push or a pull request on GitHub, Bitbucket Server, a repository of the 2nd generation, Developer Connect or Cloud Source Repositories), when a Pub/Sub message arrives, when a webhook is called or when a person runs it. The build is a list of container steps (a build block, or a file in the repository) with substitutions, images and artifacts to push, secrets from Secret Manager, a service account, a log destination and optionally a manual approval. STACKIT has no trigger or build resource. The same delivery is a workflow of STACKIT Git Pipelines (workflows written for GitHub Actions; STACKIT-managed or custom runners; secrets from STACKIT Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Nothing is converted: the trigger conditions (branch and tag expressions, file filters, the event kind), the steps (a container step becomes a step or a job with a container), the secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person. The pages read describe pipelines that run when changes are pushed; other events (pull requests, schedules, manual runs) are verified against the Forgejo Actions reference; the assessment classifies google_cloudbuild_trigger.init as redesign: only its switched-off module holds a target, and that is a proposal; 6 critical settings of `google_cloudbuild_trigger.init` are open and part of this step: `project-residency`, `build-identity`, `log-destination`, `deployment-artifact`, `entry-point`, `dep-references` (`assurance/critical-semantics.json` says why each is open and what closes it); google_cloudbuild_trigger.init exists only under a condition of the source (applies only if its count (var.init ? 1 : 0) holds when OpenTofu plans — present with the defaults of var.init (1 instance); a variable file (-var-file), -var or TF_VAR_init can change that))
63. The proposed alternative of google_cloudbuild_trigger.placeholder (`MAN/unit/google_cloudbuild_trigger.placeholder`) — manual step: Decide whether the STACKIT alternative in the switched-off module terraform/proposals/proposal_placeholder replaces google_cloudbuild_trigger.placeholder (google_cloudbuild_trigger): read the module, accept the stated difference, give it its values and set proposal_placeholder_enabled to true, or set the target up another way by hand. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-PROPOSED-ALTERNATIVE/google_cloudbuild_trigger.placeholder`; and, as the plan states: the assessment says: A Cloud Build trigger starts a build when a source event happens (a push or a pull request on GitHub, Bitbucket Server, a repository of the 2nd generation, Developer Connect or Cloud Source Repositories), when a Pub/Sub message arrives, when a webhook is called or when a person runs it. The build is a list of container steps (a build block, or a file in the repository) with substitutions, images and artifacts to push, secrets from Secret Manager, a service account, a log destination and optionally a manual approval. STACKIT has no trigger or build resource. The same delivery is a workflow of STACKIT Git Pipelines (workflows written for GitHub Actions; STACKIT-managed or custom runners; secrets from STACKIT Secrets Manager or the repository; a manual-approval action) or the customer's own CI/CD. Nothing is converted: the trigger conditions (branch and tag expressions, file filters, the event kind), the steps (a container step becomes a step or a job with a container), the secrets (created again, never copied), the service account (replaced by the credentials the workflow uses against STACKIT), the log destination and the approval are written again by a person. The pages read describe pipelines that run when changes are pushed; other events (pull requests, schedules, manual runs) are verified against the Forgejo Actions reference; the assessment classifies google_cloudbuild_trigger.placeholder as redesign: only its switched-off module holds a target, and that is a proposal; 6 critical settings of `google_cloudbuild_trigger.placeholder` are open and part of this step: `project-residency`, `build-identity`, `log-destination`, `deployment-artifact`, `dep-references`, `linked-dependency:google_firebase_hosting_site` (`assurance/critical-semantics.json` says why each is open and what closes it); google_cloudbuild_trigger.placeholder exists only under a condition of the source (applies only if its count (var.init ? 1 : 0) holds when OpenTofu plans — present with the defaults of var.init (1 instance); a variable file (-var-file), -var or TF_VAR_init can change that))

### Cutover

64. The write freeze of google_sql_database_instance.postgres by dump and restore (`MAN/data/google_sql_database_instance.postgres/write-freeze/dump-restore`) — manual step: List who writes (`SELECT usename, application_name, client_addr, count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' GROUP BY 1, 2, 3 ORDER BY 4 DESC;`) and record the connection settings of every client where they can be restored from. Then stop the writers at the application (services, jobs, migrations, cron). Do not make the source read-only and do not change it. Check: `SELECT count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' AND state <> 'idle' AND pid <> pg_backend_pid();` is 0. Runbook section: `terraform/handover/database-postgres/data-move.md#write-freeze-dump-restore`. After step 41. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 41, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path export-and-import-with-the-engine-clients of google_sql_database_instance.postgres, not of the path the draft carries out by default)
65. The final copy of the data of google_sql_database_instance.postgres by dump and restore (`MAN/data/google_sql_database_instance.postgres/final-sync/dump-restore`) — manual step: Dump inside the freeze: `PGPASSWORD="$SOURCE_PGPASSWORD" PGSSLMODE=require pg_dump --host=\<source-host> --port=\<source-port> --username=\<source-user> --dbname=\<source-db> --format=directory --jobs=4 --no-owner --no-acl --file=\<dump-dir>` (Google's page names --no-owner and --no-acl for a dump of one database and the directory format for a parallel dump; dump one database at a time, since pg_dumpall fails for databases with several owners). Restore as the database owner into the empty database: `PGPASSWORD="$TARGET_PGPASSWORD" PGSSLMODE=require pg_restore --host=\<target-host> --port=\<target-port> --username=\<target-user> --dbname=\<target-db> --jobs=4 --no-owner --no-acl \<dump-dir> 2> restore.log`, then read restore.log line by line; create the extensions of the source that the target lists and settle the ones it does not. Runbook section: `terraform/handover/database-postgres/data-move.md#final-sync-dump-restore`. After step 64. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 64, a manual prerequisite; and, as the plan states: the freeze lasts as long as the dump and the restore; this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path export-and-import-with-the-engine-clients of google_sql_database_instance.postgres, not of the path the draft carries out by default)
66. The data reconciliation of google_sql_database_instance.postgres by dump and restore (`MAN/data/google_sql_database_instance.postgres/reconcile/dump-restore`) — manual step: Both sides, with the writers stopped: per-table counts (`SELECT format('SELECT %L AS tbl, count(*) FROM %I.%I;', schemaname || '.' || tablename, schemaname, tablename) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema');` run on each side), an md5 of every keyed table (`SELECT count(*), md5(string_agg(t::text, ',' ORDER BY t.\<key-column>)) FROM \<schema>.\<table> t;`, a key range for a large table, `DateStyle`, `TimeZone` and `extra_float_digits` equal in both sessions), the sequences, and one read query the application itself runs on its most important data, `\<application-query>`: the query and the answer are the customer's, the source does not state them. Record GO or NO-GO. Runbook section: `terraform/handover/database-postgres/data-move.md#reconcile-dump-restore`. After step 65. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 65, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path export-and-import-with-the-engine-clients of google_sql_database_instance.postgres, not of the path the draft carries out by default)
67. The write freeze of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/write-freeze/logical-replication`) — manual step: List who writes (`SELECT usename, application_name, client_addr, count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' GROUP BY 1, 2, 3 ORDER BY 4 DESC;`) and record the connection settings of every client where they can be restored from. Then stop the writers at the application (services, jobs, migrations, cron). Do not make the source read-only and do not change it. Check: `SELECT count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' AND state <> 'idle' AND pid <> pg_backend_pid();` is 0. Runbook section: `terraform/handover/database-postgres/data-move.md#write-freeze-logical-replication`. After step 43. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 43, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default)
68. The final copy of the data of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/final-sync/logical-replication`) — manual step: Wait until the lag is 0. Copy the sequences: on the source `SELECT format('SELECT setval(%L, %s);', schemaname || '.' || sequencename, last_value) FROM pg_sequences WHERE last_value IS NOT NULL;` with `psql --tuples-only --no-align`, and run the output on the target. Then disable the subscription on the target: `ALTER SUBSCRIPTION stacktoit_move DISABLE;` (the slot stays on the source, so the way back stays open). Runbook section: `terraform/handover/database-postgres/data-move.md#final-sync-logical-replication`. After step 67. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 67, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default)
69. The data reconciliation of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/reconcile/logical-replication`) — manual step: Both sides, with the writers stopped: per-table counts (`SELECT format('SELECT %L AS tbl, count(*) FROM %I.%I;', schemaname || '.' || tablename, schemaname, tablename) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema');` run on each side), an md5 of every keyed table (`SELECT count(*), md5(string_agg(t::text, ',' ORDER BY t.\<key-column>)) FROM \<schema>.\<table> t;`, a key range for a large table, `DateStyle`, `TimeZone` and `extra_float_digits` equal in both sessions), the sequences, and one read query the application itself runs on its most important data, `\<application-query>`: the query and the answer are the customer's, the source does not state them. Record GO or NO-GO. Runbook section: `terraform/handover/database-postgres/data-move.md#reconcile-logical-replication`. After step 68. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 68, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default)
70. The write freeze of google_sql_database_instance.postgres with a tool of your own (`MAN/data/google_sql_database_instance.postgres/write-freeze/own-tool`) — manual step: List who writes (`SELECT usename, application_name, client_addr, count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' GROUP BY 1, 2, 3 ORDER BY 4 DESC;`) and record the connection settings of every client where they can be restored from. Then stop the writers at the application (services, jobs, migrations, cron). Do not make the source read-only and do not change it. Check: `SELECT count(*) FROM pg_stat_activity WHERE datname = '\<source-db>' AND state <> 'idle' AND pid <> pg_backend_pid();` is 0. Runbook section: `terraform/handover/database-postgres/data-move.md#write-freeze-own-tool`. After step 41. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 41, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path migrate-with-a-tool-of-my-own of google_sql_database_instance.postgres, not of the path the draft carries out by default)
71. The final copy of the data of google_sql_database_instance.postgres with a tool of your own (`MAN/data/google_sql_database_instance.postgres/final-sync/own-tool`) — manual step: Copy the data of google_sql_database_instance.postgres to stackit_postgresflex_instance.postgres with the migration tool or the replication you chose: write its steps, the checks before the move, the freeze window it needs and the way back. Runbook section: `terraform/handover/database-postgres/data-move.md#final-sync-own-tool`. After step 70. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 70, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path migrate-with-a-tool-of-my-own of google_sql_database_instance.postgres, not of the path the draft carries out by default)
72. The data reconciliation of google_sql_database_instance.postgres with a tool of your own (`MAN/data/google_sql_database_instance.postgres/reconcile/own-tool`) — manual step: Both sides, with the writers stopped: per-table counts (`SELECT format('SELECT %L AS tbl, count(*) FROM %I.%I;', schemaname || '.' || tablename, schemaname, tablename) FROM pg_tables WHERE schemaname NOT IN ('pg_catalog', 'information_schema');` run on each side), an md5 of every keyed table (`SELECT count(*), md5(string_agg(t::text, ',' ORDER BY t.\<key-column>)) FROM \<schema>.\<table> t;`, a key range for a large table, `DateStyle`, `TimeZone` and `extra_float_digits` equal in both sessions), the sequences, and one read query the application itself runs on its most important data, `\<application-query>`: the query and the answer are the customer's, the source does not state them. Record GO or NO-GO. Runbook section: `terraform/handover/database-postgres/data-move.md#reconcile-own-tool`. After step 71. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 71, a manual prerequisite; and, as the plan states: this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path migrate-with-a-tool-of-my-own of google_sql_database_instance.postgres, not of the path the draft carries out by default)
73. The write freeze of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/write-freeze/rclone-copy`) — manual step: List who writes to the bucket: the units the README lists for it, and every job, partner and signed URL outside the scan (if Cloud Audit Logs record data writes for the bucket, its object writes and deletes name the principals; the Terraform does not show whether they are). Record the endpoint, the bucket name and the credential settings of every client where they can be restored from. Then stop the writers at the application (services, jobs, uploads). Do not change the source bucket (no read-only policy, no retention lock): it stays read-write, so the way back needs nothing on the source. Runbook section: `terraform/handover/bucket-media/data-copy.md#write-freeze-rclone-copy`. After step 38. (executable once its manual prerequisite is done: it waits for step 38, a manual prerequisite)
74. The final copy of the data of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/final-sync/rclone-copy`) — manual step: Inside the freeze run the `rclone copy` of the initial copy once more and read how many objects it transferred; run it a second time: it transfers nothing. Then see what the target holds that the source no longer has: `rclone sync SRC:\<source-bucket> DST:\<target-bucket> --dry-run` prints what a sync would delete; read the list, and only a person who has read it runs the sync without `--dry-run` (the target must equal the source at the cutover, and an object deleted in the source after the initial copy is otherwise still served). Runbook section: `terraform/handover/bucket-media/data-copy.md#final-sync-rclone-copy`. After step 73. (executable once its manual prerequisite is done: it waits for step 73, a manual prerequisite)
75. The data reconciliation of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/reconcile/rclone-copy`) — manual step: Both sides, with the writers stopped: the number of objects and the bytes (`rclone size SRC:\<source-bucket> --json`, `rclone size DST:\<target-bucket> --json`); the list of keys (`rclone lsf -R --files-only SRC:\<source-bucket> | sort > source-keys.txt`, the same for the target, `diff source-keys.txt target-keys.txt` is empty); and the content: `rclone check SRC:\<source-bucket> DST:\<target-bucket> --one-way --combined=\<check-file>` compares size and hash where both sides have one and marks every difference, so read the file and the log; an object for which the two sides share no hash (the target's ETag of a multipart upload is not an MD5) is at best compared by size, which is not its content, so run `rclone check ... --download` for the prefixes that matter and for a sample of the rest. One read that the application itself does on its most important objects through the STACKIT endpoint with a key of a credentials group: `\<application-read>`, the query and the answer are the customer's, the source does not state them. Record GO or NO-GO. Runbook section: `terraform/handover/bucket-media/data-copy.md#reconcile-rclone-copy`. After step 74. (executable once its manual prerequisite is done: it waits for step 74, a manual prerequisite)

### Decommission

76. The cleanup of the data move of google_sql_database_instance.postgres by dump and restore (`MAN/data/google_sql_database_instance.postgres/cleanup/dump-restore`) — manual step: Only after the target has run in production for the period the customer decides and the way back is no longer wanted: remove the move host from `network.acl` of the instance and from the authorized networks of the source if it was added; delete the dump files and `restore.log`; keep a final backup of the source for the retention the customer decides before anything is deleted; replace the credentials of the target user if they were ever on a ticket or in a file. Runbook section: `terraform/handover/database-postgres/data-move.md#clean-up-dump-restore`. After step 66. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 66, a manual prerequisite; and, as the plan states: the source is not deleted in the same hour as the cutover; this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path export-and-import-with-the-engine-clients of google_sql_database_instance.postgres, not of the path the draft carries out by default; the plan holds no point at which the owner records GO or ROLLBACK, so nothing in it orders this cleanup after the GO: do not run it before the owner has decided that the way back is no longer wanted)
77. The cleanup of the data move of google_sql_database_instance.postgres by logical replication (`MAN/data/google_sql_database_instance.postgres/cleanup/logical-replication`) — manual step: Only after the target has run in production for the period the customer decides and the way back is no longer wanted: `DROP SUBSCRIPTION stacktoit_move;` on the target (it drops the slot on the source if the source is reachable; check `SELECT * FROM pg_replication_slots;`), `DROP PUBLICATION stacktoit_move;` and `DROP ROLE stacktoit_repl;` on the source, set `cloudsql.logical_decoding` back with a second restart if it was off before the move; remove the move host from `network.acl` of the instance and from the authorized networks of the source if it was added; delete the dump files and `restore.log`; keep a final backup of the source for the retention the customer decides before anything is deleted; replace the credentials of the target user if they were ever on a ticket or in a file. Runbook section: `terraform/handover/database-postgres/data-move.md#clean-up-logical-replication`. After step 69. (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-DATA-MOVEMENT`; it also comes after step 69, a manual prerequisite; and, as the plan states: the source is not deleted in the same hour as the cutover; this step holds only once DEC-GEN-CONVERT-DATA-MOVEMENT is answered: it is of the path replicate-with-the-engine-replication of google_sql_database_instance.postgres, not of the path the draft carries out by default; the plan holds no point at which the owner records GO or ROLLBACK, so nothing in it orders this cleanup after the GO: do not run it before the owner has decided that the way back is no longer wanted)
78. The cleanup of the data move of google_storage_bucket.media with rclone (`MAN/data/google_storage_bucket.media/cleanup/rclone-copy`) — manual step: Only after the target has run in production for the period the customer decides and the way back is no longer wanted: delete the key lists and the logs of the move (they name every object key); delete the credential made for the move, the Google login or key made for it, and take the environment names out of the shell history; keep the source bucket, with its versions, for the retention the customer decides before any object of it is deleted, and do not delete it in the same hour as the cutover. Runbook section: `terraform/handover/bucket-media/data-copy.md#clean-up-rclone-copy`. After step 75. (executable once its manual prerequisite is done: it waits for step 75, a manual prerequisite)

## Terraform commands

`execution/terraform/apply.md` lists every OpenTofu step with its resource and file. OpenTofu applies the whole draft in one run and enforces none of the manual steps, STACKIT API calls or phases of the plan: complete the steps that `execution/terraform/apply.md` lists under "Complete these steps before tofu apply" first. From the output folder:

```sh
cd terraform
tofu init
tofu validate
tofu plan -out=stackit.tfplan
tofu apply stackit.tfplan
```

## API commands

Each call has a request file (the call as data) and a script that renders it. A script is run with `bash <script>` from any folder; it stops before the call when its step is not executable, when an input is not set, or when the call needs a confirmation.

- step 15: `execution/api/secrets-manager-vault/001-writesecret.sh` (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; and, as the plan states: Required input: STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_ADMIN_PASSWORD names a file (keep it outside the package and out of version control) that holds the JSON object of the key/value pairs the secret at the path STACKIT_INPUT_SECRET_NAME_DJANGO_ADMIN_PASSWORD names has on STACKIT. StackToIT never reads the value of google_secret_manager_secret.django_admin_password from the source, so the document is yours: the same value from a secure source, or a new one. The source builds the value (its version resource, attribute secret_data) from a value it generates itself (random_password.django_admin_password): the document holds the matching values on STACKIT (for a generated value, one you generate or choose for STACKIT: the generated one is only in the source's state), under the keys the consumers of the secret read (the key names are in the source's secret_data, which the scan does not read); To make the document from Google Cloud, if the value is still there, run this yourself with your own Google credentials (StackToIT never runs it): `gcloud secrets versions access latest --secret=\<the Google secret id> --project=\<the Google project> --out-file=\<a file outside the package>`, put its content under the key the applications read it under in a JSON object (`{"\<key>": "\<the content>"}`) in the file STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_ADMIN_PASSWORD names, and delete both files after the write; The path of the secret is an input: STACKIT_INPUT_SECRET_NAME_DJANGO_ADMIN_PASSWORD names it (the source does not state the name as a literal the plan can rely on), so this step does not carry the setting `name` and its manual step stays; the applications read the secret by the path you choose; The step logs in as the migration user stackit_secretsmanager_user.migration (write_enabled = true), which reaches every secret of stackit_secretsmanager_instance.migrated_secrets and whose generated password is in the OpenTofu state (a sensitive attribute): delete the user (remove its block from the draft and apply) once the secrets are written. The script reads the username and password with tofu output, or from STACKIT_INPUT_USERNAME and STACKIT_INPUT_PASSWORD, and prints neither; The write makes a new version of the secret. The script reads the secret first (ReadSecret) and stops without writing (exit 4) when it already has a current version, unless STACKIT_CONFIRM_REPLACE is set to the path of the secret; it does not read the values back and prints none; STACKIT's page shows neither the answer of the login nor the status of a read of a secret that has no version: the script takes the token from auth.client_token and reads 404 as "no current version" (the Vault KV version 2 API the page calls compatible), and stops on any other answer. The API states no release status, so the call waits for the opt-in to it; the maturity of the Secrets Manager Vault-compatible API is not established (its documentation states no release status): a call of it is not executed without an explicit opt-in (DEC-GEN-CONVERT-API-OPT-IN); the migration of `google_secret_manager_secret.django_admin_password` is not complete with this step: 3 critical settings of it that the draft does not carry are open in 2 steps, a step standing for several of them (3 manual, 0 refused): `secret-value`, `secret-settings`, `tcr-secret-id`)
- step 16: `execution/api/secrets-manager-vault/002-writesecret.sh` (not executable — requires a design decision — blocked by `DEC-GEN-CONVERT-API-OPT-IN/google_secret_manager_secret.django_admin_password`; and, as the plan states: Required input: STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_SETTINGS names a file (keep it outside the package and out of version control) that holds the JSON object of the key/value pairs the secret at the path STACKIT_INPUT_SECRET_NAME_DJANGO_SETTINGS names has on STACKIT. StackToIT never reads the value of google_secret_manager_secret.django_settings from the source, so the document is yours: the same value from a secure source, or a new one. The source builds the value (its version resource, attribute secret_data) from google_sql_database.database, google_sql_database_instance.postgres, google_sql_user.django, google_storage_bucket.media and a value it generates itself (random_password.django_secret_key): the document holds the matching values on STACKIT (for a generated value, one you generate or choose for STACKIT: the generated one is only in the source's state), under the keys the consumers of the secret read (the key names are in the source's secret_data, which the scan does not read); To make the document from Google Cloud, if the value is still there, run this yourself with your own Google credentials (StackToIT never runs it): `gcloud secrets versions access latest --secret=\<the Google secret id> --project=\<the Google project> --out-file=\<a file outside the package>`, put its content under the key the applications read it under in a JSON object (`{"\<key>": "\<the content>"}`) in the file STACKIT_INPUT_SECRET_DOCUMENT_DJANGO_SETTINGS names, and delete both files after the write; The path of the secret is an input: STACKIT_INPUT_SECRET_NAME_DJANGO_SETTINGS names it (the source does not state the name as a literal the plan can rely on), so this step does not carry the setting `name` and its manual step stays; the applications read the secret by the path you choose; The step logs in as the migration user stackit_secretsmanager_user.migration (write_enabled = true), which reaches every secret of stackit_secretsmanager_instance.migrated_secrets and whose generated password is in the OpenTofu state (a sensitive attribute): delete the user (remove its block from the draft and apply) once the secrets are written. The script reads the username and password with tofu output, or from STACKIT_INPUT_USERNAME and STACKIT_INPUT_PASSWORD, and prints neither; The write makes a new version of the secret. The script reads the secret first (ReadSecret) and stops without writing (exit 4) when it already has a current version, unless STACKIT_CONFIRM_REPLACE is set to the path of the secret; it does not read the values back and prints none; STACKIT's page shows neither the answer of the login nor the status of a read of a secret that has no version: the script takes the token from auth.client_token and reads 404 as "no current version" (the Vault KV version 2 API the page calls compatible), and stops on any other answer. The API states no release status, so the call waits for the opt-in to it; the maturity of the Secrets Manager Vault-compatible API is not established (its documentation states no release status): a call of it is not executed without an explicit opt-in (DEC-GEN-CONVERT-API-OPT-IN); the migration of `google_secret_manager_secret.django_settings` is not complete with this step: 3 critical settings of it that the draft does not carry are open in 2 steps, a step standing for several of them (3 manual, 0 refused): `secret-value`, `secret-settings`, `tcr-secret-id`)

The execution order names, as "Step state", how far the step of a call has come: "generated, not yet tested offline" (a converter wrote it and no test ran its script against a local stub), "generated, tested offline" (a test of StackToIT ran the script under a real shell against a local stub), "verified live" (a recorded probe against the STACKIT API shows the step does what it states) and "catalogued, not generated" (the call is known and described and no converter writes it). StackToIT runs none of these calls itself.

A script compares the answer of its call with the answer the plan expects (the HTTP status exactly, and a member when the plan names one). It stops with exit 3 when its step is not executable, 4 when the call needs the confirmation, 5 when an input, a tool or a file is missing or unsafe, 6 when its lookup cannot tell whether the object exists, 7 when the answer is not the expected one, and 8 when a postcondition it checks does not hold.

A script that calls a STACKIT API with curl trusts the certificate authorities curl trusts. For an API behind a private authority, set `STACKIT_CA_BUNDLE` to a PEM file of its certificates (curl's --cacert); the certificate is always verified. Each call ends after `STACKIT_CALL_TIMEOUT_SECONDS` seconds (120 unless it names others, at most 3600), so an API that does not answer stops the script with a non-zero exit instead of holding it; a call that timed out may still have been made, so read the target before running the script again.

A script that writes a secret into STACKIT Secrets Manager (the Vault-compatible API) logs in as the Secrets Manager user the draft creates, with the username and password that user has once the draft is applied (the script reads them from the draft's outputs with tofu output, or from its `STACKIT_INPUT_USERNAME` and `STACKIT_INPUT_PASSWORD` variables), and writes the document of key/value pairs that the file named in its `STACKIT_INPUT_SECRET_DOCUMENT_<n>` variable holds. The document is a file of yours: the package holds no value of any secret, and the script prints no value, puts none on a command line and keeps none outside its own temporary folder. A secret that already has a current version stops the script (exit 4) until `STACKIT_CONFIRM_REPLACE` is set to the path of the secret, because the write makes a new version the current one. The user is a migration user with write access to every secret of the instance and its password is in the OpenTofu state: delete the user (remove its block from the draft and apply) once the secrets are written.

## Manual steps

64 steps are carried out by a person, each described in `execution/manual/actions.md`: what, why, who, when, what it needs and when it is done. Roles named: Data Architect, Identity Architect, Migration Owner, Network Architect, Platform Architect, Platform Engineer, Security Architect, Storage Architect.

## Verification

`execution/verify/checks.md` lists 72 checks to hold before the steps and 12 checks to hold after them, one checklist row each.

## Rollback

Undoing a step: 10 can be undone (the step says how), 2 only in part (what was written since is lost), 66 by a person, 0 cannot be undone. From the cutover on, steps 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78 cannot be undone in full by a tool: decide how before the cutover starts. Each step's rollback is in the actions sheet and, for a manual step, in `execution/manual/actions.md`.

## Files of this package

- `execution-plan.json` — the plan itself, machine-readable, at the root of the run: the one file of the plan (the package holds no copy of it)
- `execution/terraform/apply.md` — the OpenTofu steps and commands
- `execution/api/` — one request file and one script for each STACKIT API call
- `execution/manual/actions.md` — the manual steps and what is not migrated
- `execution/verify/checks.md` — the checks before and after each step
- `migration/execution-actions.csv` — one row per step, for a spreadsheet
- `migration/work-packages.csv` — one row per work package of the manual steps, for a spreadsheet
