DEV Community

iapilgrim
iapilgrim

Posted on

GCP The Hard Way — Part 1: Migrating a Legacy Application Without Touching Its Code

Introduction

A common first step in cloud adoption is the "lift-and-shift" migration:
moving an existing application to the cloud with minimal changes to
reduce migration risk and preserve the original release cadence. In
this post, we walk through migrating a application with a hardcoded
localhost database dependency to Google Cloud, without modifying a
single line of application code — relying instead on infrastructure
and configuration to bridge the gap.

By the end of this walkthrough, you will have deployed a Compute Engine
instance connected to a Cloud SQL database over a private network path,
with no public database endpoint exposed.

Solution overview

The target architecture places the application on a Compute Engine VM
and the database on Cloud SQL, connected through the Cloud SQL Auth
Proxy running as a sidecar process on the VM. This preserves the
application's original localhost connection string while routing
traffic securely to the managed database.

┌─────────────────────────────┐
│      Compute Engine VM       │
│  ┌────────┐   ┌───────────┐  │        ┌───────────────┐
│  │  App   │──▶│ Cloud SQL │──┼───────▶│   Cloud SQL    │
│  │        │   │Auth Proxy │  │Private │ (Private IP)   │
│  └────────┘   └───────────┘  │  VPC   └───────────────┘
└─────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Prerequisites

  • A GCP project with the Compute Engine and Cloud SQL Admin APIs enabled
  • A sample application (Node.js, Flask, or WordPress) that reads its database host from a config file or environment variable set to localhost
  • The application verified working in a local environment before migration begins

Walkthrough

Step 1: Provision the database and compute resources

gcloud services enable compute.googleapis.com sqladmin.googleapis.com

gcloud sql instances create app-db \
  --database-version=MYSQL_8_0 \
  --tier=db-f1-micro \
  --region=asia-southeast1 \
  --no-assign-ip \
  --network=default

gcloud sql databases create appdb --instance=app-db

gcloud compute instances create app-vm \
  --zone=asia-southeast1-a \
  --machine-type=e2-small \
  --image-family=debian-12 \
  --image-project=debian-cloud
Enter fullscreen mode Exit fullscreen mode

We disable the public IP on the Cloud SQL instance from the start
(--no-assign-ip), reflecting a common enterprise security requirement
that databases never be reachable over the public internet.

Step 2: Establish private connectivity

Cloud SQL private IP requires a VPC peering connection to Google's
managed services range:

gcloud compute addresses create google-managed-services-default \
  --global --purpose=VPC_PEERING --prefix-length=16 --network=default

gcloud services vpc-peerings connect \
  --service=servicenetworking.googleapis.com \
  --ranges=google-managed-services-default \
  --network=default
Enter fullscreen mode Exit fullscreen mode

Step 3: Deploy the Cloud SQL Auth Proxy on the VM

gcloud compute ssh app-vm --zone=asia-southeast1-a
Enter fullscreen mode Exit fullscreen mode

On the VM:

curl -o cloud-sql-proxy \
  https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.11.0/cloud-sql-proxy.linux.amd64
chmod +x cloud-sql-proxy
./cloud-sql-proxy --private-ip <PROJECT_ID>:asia-southeast1:app-db &
Enter fullscreen mode Exit fullscreen mode

The proxy binds to 127.0.0.1:3306 by default, which is exactly the
endpoint the application already expects.

Step 4: Open the required firewall path

Applications frequently listen on ports other than 80/443. Configure
the firewall to allow only the specific port the application requires:

gcloud compute instances add-tags app-vm --tags=app-server --zone=asia-southeast1-a

gcloud compute firewall-rules create allow-app-port \
  --allow=tcp:3000 \
  --source-ranges=0.0.0.0/0 \
  --target-tags=app-server
Enter fullscreen mode Exit fullscreen mode

Common pitfall: a connection timeout at this stage can originate
from either a missing firewall rule or an application that binds to
127.0.0.1 instead of 0.0.0.0. Verify with curl localhost:3000
from inside the VM before troubleshooting the network path.

Step 5: Attach a custom domain

gcloud dns managed-zones create app-zone \
  --dns-name="yourdomain.com." \
  --description="Lift and shift walkthrough"
Enter fullscreen mode Exit fullscreen mode

Create an A record pointing to the VM's external IP through the
Cloud Console or gcloud dns record-sets create.

Validating the deployment

Confirm the following before considering the migration complete:

  • gcloud sql instances describe app-db shows no public IP address
  • The application is reachable via the custom domain, not the raw IP
  • No port other than 80/443 is exposed to 0.0.0.0/0

Clean up resources

gcloud compute instances delete app-vm --zone=asia-southeast1-a --quiet
gcloud sql instances delete app-db --quiet
gcloud compute firewall-rules delete allow-app-port --quiet
gcloud dns managed-zones delete app-zone --quiet
Enter fullscreen mode Exit fullscreen mode

Conclusion

This walkthrough demonstrated that a lift-and-shift migration is
rarely "zero infrastructure change" even when it is "zero code change."
The Cloud SQL Auth Proxy pattern shown here is a common bridge for
applications that cannot be immediately refactored to use
cloud-native service discovery.

In Part 2, we build on this foundation with a highly available
three-tier architecture — and deliberately break it five different
ways to practice production diagnostics.

Top comments (0)