Cloud Deployment Manager is Google Cloud's original infrastructure-as-code service: YAML configs, Jinja or Python templates, and a server that creates, updates and deletes resources to match. It is also being retired. According to Google's deprecation page, support was discontinued on 1 April 2026, new users have been blocked from enabling the API or creating a first deployment since 30 June 2026, and after 30 June 2027 the service will be turned down. Resources that Deployment Manager created keep working; you simply lose the ability to manage them through it.
So this article is for teams that still have deployments, not for anyone starting one. It explains how the service works, which you need in order to read old configs and change them safely in the meantime, and then how to inventory and migrate deployments to Terraform under Infrastructure Manager or your own pipeline without recreating or orphaning anything.
Deployments, configs, templates and manifests
A deployment is a named set of resources in one project. You describe it in a config file whose resources list gives each resource a name, a type such as compute.v1.network or storage.v1.bucket, and properties that mirror the underlying API's request body. A config can import Jinja or Python templates; a resource whose type is a template file is expanded into more resources.
Unlike Terraform, there is no state file on your side. Each create or update produces a manifest on the server: a read-only record holding the original config, the imported templates, the expandedConfig after all templates ran, and a layout. The manifest is the closest thing to state, and it is what you will migrate from.
Resources refer to each other with $(ref.NAME.FIELD), for example $(ref.app-network.selfLink). A reference both inserts the value and creates an ordering dependency. metadata.dependsOn adds ordering without a value.
imports:
- path: vm_template.jinja
resources:
- name: app-network
type: compute.v1.network
properties:
autoCreateSubnetworks: false
- name: app-subnet
type: compute.v1.subnetwork
properties:
region: us-central1
network: $(ref.app-network.selfLink)
ipCidrRange: 10.10.0.0/24
- name: app-vm
type: vm_template.jinja
properties:
zone: us-central1-a
subnet: $(ref.app-subnet.selfLink)
- name: app-logs
type: storage.v1.bucket
properties:
location: US
accessControl:
gcpIamPolicy:
bindings:
- role: roles/storage.objectViewer
members:
- group:sre@example.comThe template receives the resource's properties and an env map that includes the resource name and the deployment name, which is why the converter later asks for the deployment name:
resources:
- name: {{ env["name"] }}
type: compute.v1.instance
properties:
zone: {{ properties["zone"] }}
machineType: zones/{{ properties["zone"] }}/machineTypes/e2-small
disks:
- deviceName: boot
boot: true
autoDelete: true
initializeParams:
sourceImage: projects/debian-cloud/global/images/family/debian-12
networkInterfaces:
- subnetwork: {{ properties["subnet"] }}
How an update is applied
On an update, the server expands the templates, compares the new expanded config with the current manifest, and calls each resource's API in dependency order. It acts using the project's Google APIs service agent, PROJECT_NUMBER@cloudservices.gserviceaccount.com, not your own identity, so a config can create resources the person running it could not create directly. Do not remove that agent's role as part of a migration; managed instance groups and other services also rely on it.
Two policies control what an update does to resources. --create-policy decides whether a resource named in the config may be created or must already exist; ACQUIRE adopts existing resources without creating anything. --delete-policy decides what happens to resources dropped from the config or the whole deployment: DELETE, the default, deletes them; ABANDON removes them from the deployment and leaves them running. That default is the most dangerous fact in this article: deleting a deployment without --delete-policy=ABANDON deletes the networks, VMs and buckets in it.
Deployment Manager does not reconcile continuously. A change made in the console stays until the next update, which may overwrite it or fail on it. Preview every update, even small ones, while you still run them:
# Stage the change; nothing is modified yet.
gcloud deployment-manager deployments update app --config app.yaml --preview
# Read what the server plans to do, resource by resource.
gcloud deployment-manager deployments describe app
# Then either apply the staged preview ...
gcloud deployment-manager deployments update app
# ... or throw it away.
gcloud deployment-manager deployments cancel-preview appLiving with deployments until they move
Until a deployment has moved, treat it as frozen infrastructure that occasionally needs a careful change. Google's page says you can still use the service to manage, migrate or delete existing resources at your own risk, that standard support tickets are rejected unless they concern a blocker or the migration itself, and that the maximum extension of support ends on 31 March 2027. No new features or non-critical fixes are coming.
In practice that means a few rules. Limit who can run updates to a small group, because every update now runs without a support path if it fails halfway. Make every change through a preview, read it, and keep the saved expanded config in version control alongside the change. Never delete a deployment except with ABANDON. Make emergency fixes, such as a firewall rule during an incident, directly on the resource, write them down, and fold them into the converted Terraform later, rather than trying to push an urgent update through templates nobody has run for two years. And do not start new deployments: build anything new in Terraform from the start, even if it lives next to resources that Deployment Manager still manages.
Inventory before you migrate
Start by finding every deployment. Most projects never enabled the API, so errors from those can be ignored:
for p in $(gcloud projects list --format="value(projectId)"); do
gcloud deployment-manager deployments list --project "$p" \
--format="value(name)" 2>/dev/null | sed "s|^|$p,|"
done > dm_deployments.csvFor each one, save the expanded config from its latest manifest. These are the commands from Google's DM Convert guide; the manifest name is the last path segment of the first command's output:
gcloud deployment-manager deployments describe "$DEPLOYMENT" \
--project "$PROJECT" --format="value(deployment.manifest)"
gcloud deployment-manager manifests describe "$MANIFEST" \
--deployment "$DEPLOYMENT" --project "$PROJECT" \
--format="value(expandedConfig)" > "$PROJECT.$DEPLOYMENT.yaml"Then classify each saved file. The converter handles ordinary resource types, references, dependsOn and IAM, but not everything:
| Found in the config | What DM Convert does with it for Terraform |
|---|---|
References $(ref...) | Translated into Terraform references |
metadata.dependsOn | Translated into depends_on |
accessControl (authoritative) | *_iam_policy resources, also authoritative |
iamMemberBinding types | *_iam_member resources |
| Composite types | Not converted; they are already deprecated |
| Custom type providers and outputs | Not converted |
| Actions | Insert calls become resources and get calls become data sources; patch, delete, list and custom calls are not converted |
Deployments containing only the first four rows are mechanical work. The others need a person: rewrite the logic by hand or replace the action with a declarative resource. Google's migration guides include replacing the setIamPolicy action and converting composite types to templates. Run the converter's --list-supported-types flag to check types against the current tool.
Converting with DM Convert
DM Convert ships as a container image and is itself a Preview tool, so review its output like code written by someone else. Run it in the directory holding the saved config:
export PROJECT_ID=my-project
export DM_CONVERT_IMAGE="us-central1-docker.pkg.dev/dm-convert-host/deployment-manager/dm-convert:public-preview"
docker run --rm -it --workdir=/convert --volume="$(pwd)":/convert \
"$DM_CONVERT_IMAGE" \
--config deployment.yaml \
--output_format TF \
--output_file main.tf \
--output_tf_import_file imports.sh \
--deployment_name app \
--project_id "$PROJECT_ID"The output is a Terraform configuration and a file of import commands. The safe order is:
- Freeze the deployment: remove permission to update it from people and CI, so two tools are never changing the same resources.
- Re-run a preview of the current config. If it plans changes, the deployment has drifted; settle that first so the conversion matches what is actually running.
- Initialise Terraform with a remote state backend in a Cloud Storage bucket, then run the import commands.
- Run
terraform plan. The only acceptable result is no changes. Every proposed change is a difference between the converted code and reality; fix the code, never the resource, until the plan is empty. - Delete the deployment with
--delete-policy=ABANDON, then confirm the resources still exist. - Hand the configuration to Infrastructure Manager or to your pipeline, and require the first preview there to be empty as well.
Running the result in Infrastructure Manager
Infrastructure Manager is Google's managed Terraform runner. It runs Terraform as a service account you name, keeps state for you and records each revision. A deployment from a local directory looks like this:
gcloud infra-manager deployments apply \
projects/my-project/locations/us-central1/deployments/app \
--service-account projects/my-project/serviceAccounts/infra-app@my-project.iam.gserviceaccount.com \
--local-source="./app-terraform" \
--input-values=region=us-central1It also accepts a Cloud Storage object or a Git repository as source. The service account replaces the broad service agent Deployment Manager used, so this is the moment to grant only the roles the configuration needs; IAM roles and principals covers custom roles and impersonation. If you already run Terraform from a pipeline such as Cloud Build, the converted code works there too, with state in a versioned bucket.
Worked example: migrating the app deployment
Take the app deployment above. The inventory finds four resources and one template, no actions, no type providers. The converter produces a network, a subnet, an instance and a bucket, with the subnet referring to the network as a Terraform reference, plus a google_storage_bucket_iam_policy for the accessControl block.
The first plan after import shows two problems. The instance has a label added in the console months ago, so Terraform wants to remove it; add the label to the code. And the IAM policy would replace the bucket's whole policy with the single binding from the old config, deleting a binding another team added for their data pipeline. The converter was faithful, since accessControl was authoritative in Deployment Manager too, but the old deployment had not been updated since that binding was added. Rewrite it as one google_storage_bucket_iam_member per binding you own and leave others alone.
The plan is now empty. You abandon the deployment, list the bucket and the VM to confirm they exist, and apply the code through Infrastructure Manager, whose preview also shows no changes. Nothing was recreated, and no request to these services failed during the migration.
Failure modes
| Failure | Consequence | Prevention |
|---|---|---|
| Deleting with the default policy | Every resource in the deployment is deleted | Always pass --delete-policy=ABANDON |
| Abandoning before the import works | Unmanaged resources nobody tracks | Abandon only after an empty plan |
| Authoritative IAM converted as is | Bindings added outside the deployment disappear | Prefer member resources for bindings you own |
| Converting a drifted deployment | Plan wants to undo real changes | Preview first; reconcile drift before converting |
| Both tools active at once | Each reverts the other | Freeze updates during the migration |
| Actions or type providers ignored | Behaviour silently missing after migration | Classify every config before converting |
| Waiting until 2027 | No support for failures during a forced migration | Migrate now, one deployment at a time |
Trade-offs
Infrastructure Manager gives you managed state, revision history and a Google-supported path, but it runs only Terraform and only from the sources it accepts. Self-run Terraform offers full control of versions, modules and pipelines, at the cost of looking after state and credentials. DM Convert can also produce Kubernetes Resource Model output for Config Connector, which suits teams that already run infrastructure through Kubernetes and want continuous reconciliation; note the converter does not carry explicit dependsOn ordering into that format. For VM fleets created from templates, compare the converted instance with managed instance groups built on instance templates before deciding to keep a single hand-managed VM.
What to do next
- Run the inventory loop across every project and list each deployment with an owner.
- Save each expanded config and classify it: plain resources, IAM, actions, type providers, composite types.
- Pick the smallest plain deployment, freeze it, preview it and convert it with DM Convert.
- Import into Terraform with remote state and do not continue until the plan is empty.
- Review every converted IAM policy and switch to member resources where others share the resource.
- Abandon the deployment, check the resources, and apply through Infrastructure Manager with a least-privilege service account.
- Schedule the remaining deployments well before 30 June 2027, hardest last.