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.com

The 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 app
Top: how Deployment Manager applies a config. Bottom: the way out, one deployment at a timeConfig YAMLresources, importsJinja / PythontemplatesExpansionserver sideManifestexpandedConfig, layoutResource API callsCompute, Storage, IAMordered by refsexpandedConfigsaved to diskmanifests describeDM ConvertPreview containerTerraform + importsplan must be emptyAbandon deploymentresources stayInfra Manageror your own TerraformThe service is unsupported since 1 April 2026 and is turned down after 30 June 2027; the resources themselves keep running.
Deployment Manager expands templates on the server and records a manifest; migration starts from that manifest and ends with an abandoned deployment.

Living 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.csv

For 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 configWhat DM Convert does with it for Terraform
References $(ref...)Translated into Terraform references
metadata.dependsOnTranslated into depends_on
accessControl (authoritative)*_iam_policy resources, also authoritative
iamMemberBinding types*_iam_member resources
Composite typesNot converted; they are already deprecated
Custom type providers and outputsNot converted
ActionsInsert 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:

  1. Freeze the deployment: remove permission to update it from people and CI, so two tools are never changing the same resources.
  2. 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.
  3. Initialise Terraform with a remote state backend in a Cloud Storage bucket, then run the import commands.
  4. 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.
  5. Delete the deployment with --delete-policy=ABANDON, then confirm the resources still exist.
  6. 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-central1

It 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

FailureConsequencePrevention
Deleting with the default policyEvery resource in the deployment is deletedAlways pass --delete-policy=ABANDON
Abandoning before the import worksUnmanaged resources nobody tracksAbandon only after an empty plan
Authoritative IAM converted as isBindings added outside the deployment disappearPrefer member resources for bindings you own
Converting a drifted deploymentPlan wants to undo real changesPreview first; reconcile drift before converting
Both tools active at onceEach reverts the otherFreeze updates during the migration
Actions or type providers ignoredBehaviour silently missing after migrationClassify every config before converting
Waiting until 2027No support for failures during a forced migrationMigrate 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

  1. Run the inventory loop across every project and list each deployment with an owner.
  2. Save each expanded config and classify it: plain resources, IAM, actions, type providers, composite types.
  3. Pick the smallest plain deployment, freeze it, preview it and convert it with DM Convert.
  4. Import into Terraform with remote state and do not continue until the plan is empty.
  5. Review every converted IAM policy and switch to member resources where others share the resource.
  6. Abandon the deployment, check the resources, and apply through Infrastructure Manager with a least-privilege service account.
  7. Schedule the remaining deployments well before 30 June 2027, hardest last.
Key takeaway: Deployment Manager has been unsupported since April 2026 and will be turned down after June 2027, but its resources keep running. Inventory every deployment, save each expanded config, convert it with DM Convert, import into Terraform until the plan is empty, rewrite authoritative IAM with care, abandon rather than delete the deployment, and run the code in Infrastructure Manager.