HA Infrastructure Manager Template — Shared VPC
TLDR - Quick Summary
What: Deploy SFTP Gateway Professional HA using a VPC that lives in a separate GCP host project (Shared VPC / XPN)
When: Your organization centralizes network resources in a dedicated shared-network project and deploys workloads into separate service projects
Difference from standard HA template: Adds a
host_projectvariable; existing VPC/subnet are required (no new VPC creation path); firewall rules deploy to the host project; two additional IM service account role grants needed in the host project
Overview
In GCP Shared VPC, one project is the host project — it owns the VPC, subnets, and firewall rules. Other projects (called service projects) are attached to the host and launch resources into its subnets. This article covers setting up the shared network infrastructure, then deploying SFTP Gateway into the service project using it.
| Resource | Where it lives |
|---|---|
| VPC, subnet, firewall rules | Host project |
| Cloud SQL, GCS bucket, secrets, load balancer, MIG | Service project |
| SFTP Gateway service account | Service project |
Prerequisites
- GCP SDK (
gcloud) installed and authenticated - Owner or Editor on both the host project and service project
roles/compute.xpnAdminat the organization level — required only for Steps 1 and 2 (enabling Shared VPC and attaching the service project). This is an org-level role; if you don't have it, ask your GCP org admin to run those two commands or grant you the role.- Both projects must have billing enabled
Step 1: Enable Shared VPC on the Host Project
Requires
roles/compute.xpnAdminat the org level.
HOST_PROJECT="your-host-project-id"
gcloud compute shared-vpc enable "${HOST_PROJECT}"
Validate:
gcloud compute shared-vpc organizations list-host-projects \
$(gcloud projects describe "${HOST_PROJECT}" --format='value(parent.id)') \
--filter="name:${HOST_PROJECT}" \
--format="table(name,xpnProjectStatus)"
Expected output — xpnProjectStatus should be HOST:
NAME XPN_PROJECT_STATUS
your-host-project-id HOST
Step 2: Attach the Service Project
Requires
roles/compute.xpnAdminat the org level.
SERVICE_PROJECT="your-service-project-id"
gcloud compute shared-vpc associated-projects add "${SERVICE_PROJECT}" \
--host-project="${HOST_PROJECT}"
Validate:
gcloud compute shared-vpc associated-projects list "${HOST_PROJECT}"
Expected output — your service project should appear:
RESOURCE_ID RESOURCE_TYPE
your-service-project-id PROJECT
Step 3: Enable Required APIs
Host project (Compute Engine is the only one needed there):
gcloud services enable compute.googleapis.com \
--project="${HOST_PROJECT}"
Service project (all required for the HA template):
gcloud services enable \
compute.googleapis.com \
sqladmin.googleapis.com \
servicenetworking.googleapis.com \
secretmanager.googleapis.com \
cloudresourcemanager.googleapis.com \
iam.googleapis.com \
config.googleapis.com \
--project="${SERVICE_PROJECT}"
Validate:
gcloud services list --project="${SERVICE_PROJECT}" --enabled \
--filter="name:(compute.googleapis.com OR sqladmin.googleapis.com OR servicenetworking.googleapis.com OR secretmanager.googleapis.com OR cloudresourcemanager.googleapis.com OR iam.googleapis.com OR config.googleapis.com)" \
--format="table(name)"
All seven services should appear in the output.
Step 4: Create the Shared VPC and Subnet
Use a custom-mode VPC (not auto-mode) — auto-mode creates subnets in every region and can cause IP range conflicts.
REGION="us-east1"
VPC_NAME="sftpgw-shared-vpc"
SUBNET_NAME="sftpgw-subnet"
SUBNET_CIDR="10.10.0.0/24"
# Create the VPC
gcloud compute networks create "${VPC_NAME}" \
--project="${HOST_PROJECT}" \
--subnet-mode=custom \
--bgp-routing-mode=regional
# Create the subnet with Private Google Access enabled
# Required for instances to reach Secret Manager, Cloud SQL, and other
# Google APIs without an external IP address
gcloud compute networks subnets create "${SUBNET_NAME}" \
--project="${HOST_PROJECT}" \
--network="${VPC_NAME}" \
--region="${REGION}" \
--range="${SUBNET_CIDR}" \
--enable-private-ip-google-access
Validate:
gcloud compute networks describe "${VPC_NAME}" \
--project="${HOST_PROJECT}" \
--format="table(name,subnetMode,routingConfig.routingMode)"
gcloud compute networks subnets describe "${SUBNET_NAME}" \
--project="${HOST_PROJECT}" \
--region="${REGION}" \
--format="table(name,ipCidrRange,privateIpGoogleAccess,region)"
Expected — privateIpGoogleAccess must be True:
NAME IP_CIDR_RANGE PRIVATE_IP_GOOGLE_ACCESS REGION
sftpgw-subnet 10.10.0.0/24 True us-east1
Step 5: Configure Cloud SQL Private Service Access
Cloud SQL uses a private IP that requires VPC peering with Google's service networking. This is configured once per VPC.
If you already have a Cloud SQL instance using a private IP on this VPC, skip this step — the peering already exists.
RANGE_NAME="sftpgw-cloudsql-ip-range"
# Allocate an internal IP range for Cloud SQL
gcloud compute addresses create "${RANGE_NAME}" \
--project="${HOST_PROJECT}" \
--global \
--purpose=VPC_PEERING \
--prefix-length=16 \
--network="${VPC_NAME}"
# Create the private service access peering connection
gcloud services vpc-peerings connect \
--project="${HOST_PROJECT}" \
--service=servicenetworking.googleapis.com \
--network="${VPC_NAME}" \
--ranges="${RANGE_NAME}"
Validate:
# Confirm the IP range is reserved
gcloud compute addresses describe "${RANGE_NAME}" \
--project="${HOST_PROJECT}" \
--global \
--format="table(name,purpose,prefixLength,status)"
Expected — status must be RESERVED:
NAME PURPOSE PREFIX_LENGTH STATUS
sftpgw-cloudsql-ip-range VPC_PEERING 16 RESERVED
# Confirm the peering connection is active
gcloud services vpc-peerings list \
--project="${HOST_PROJECT}" \
--network="${VPC_NAME}"
Expected — servicenetworking-googleapis-com peering should appear with your range:
network: projects/HOST_PROJECT_NUMBER/global/networks/sftpgw-shared-vpc
peering: servicenetworking-googleapis-com
reservedPeeringRanges:
- sftpgw-cloudsql-ip-range
service: services/servicenetworking.googleapis.com
Step 6: Create the Infrastructure Manager Service Account
Create the service account in the service project and grant it roles in both projects.
# Create the service account
gcloud iam service-accounts create infra-manager-sa \
--display-name="Infrastructure Manager Service Account" \
--project="${SERVICE_PROJECT}"
IM_SA="infra-manager-sa@${SERVICE_PROJECT}.iam.gserviceaccount.com"
# Grant service project roles
for ROLE in \
roles/compute.admin \
roles/iam.serviceAccountAdmin \
roles/iam.serviceAccountUser \
roles/cloudsql.admin \
roles/storage.admin \
roles/secretmanager.admin \
roles/servicenetworking.networksAdmin \
roles/resourcemanager.projectIamAdmin \
roles/config.agent; do
gcloud projects add-iam-policy-binding "${SERVICE_PROJECT}" \
--member="serviceAccount:${IM_SA}" \
--role="${ROLE}"
done
# Grant host project roles
# compute.networkUser — allows Terraform to reference the shared subnet in instance templates
# compute.securityAdmin — allows Terraform to create/delete firewall rules in the host project
for ROLE in \
roles/compute.networkUser \
roles/compute.securityAdmin; do
gcloud projects add-iam-policy-binding "${HOST_PROJECT}" \
--member="serviceAccount:${IM_SA}" \
--role="${ROLE}"
done
Then initialize the Infrastructure Manager service identity and allow it to impersonate the service account:
PROJECT_NUMBER=$(gcloud projects describe "${SERVICE_PROJECT}" --format='value(projectNumber)')
# Create the IM service identity
gcloud --quiet beta services identity create \
--service=config.googleapis.com \
--project="${SERVICE_PROJECT}"
# Grant it the ability to act as the deployment service account
gcloud iam service-accounts add-iam-policy-binding "${IM_SA}" \
--member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-config.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountTokenCreator" \
--project="${SERVICE_PROJECT}"
Validate:
# Verify service project roles (should list all 9 roles)
gcloud projects get-iam-policy "${SERVICE_PROJECT}" \
--flatten="bindings[].members" \
--filter="bindings.members:${IM_SA}" \
--format="table(bindings.role)"
Expected:
ROLE
roles/cloudsql.admin
roles/compute.admin
roles/config.agent
roles/iam.serviceAccountAdmin
roles/iam.serviceAccountUser
roles/resourcemanager.projectIamAdmin
roles/secretmanager.admin
roles/servicenetworking.networksAdmin
roles/storage.admin
# Verify host project roles (should list both)
gcloud projects get-iam-policy "${HOST_PROJECT}" \
--flatten="bindings[].members" \
--filter="bindings.members:${IM_SA}" \
--format="table(bindings.role)"
Expected:
ROLE
roles/compute.networkUser
roles/compute.securityAdmin
# Verify IM service identity impersonation
gcloud iam service-accounts get-iam-policy "${IM_SA}" \
--project="${SERVICE_PROJECT}" \
--flatten="bindings[].members" \
--filter="bindings.members:service-${PROJECT_NUMBER}@gcp-sa-config.iam.gserviceaccount.com" \
--format="table(bindings.role)"
Expected:
ROLE
roles/iam.serviceAccountTokenCreator
Step 7: Deploy with Infrastructure Manager
Create a working directory with the Shared VPC HA template files (sftpgw-ha.tf and terraform.tfvars). The template is in the cross-project-vpc/ folder of the infrastructure manager template repository.
Key differences in terraform.tfvars from the standard HA template:
# Service project — where SFTP Gateway resources deploy
project = "your-service-project-id"
# Host project — where the shared VPC lives
host_project = "your-host-project-id"
# Name of the shared VPC in host_project
existing_network = "sftpgw-shared-vpc"
# Name of the shared subnet in host_project
existing_subnet = "sftpgw-subnet"
Run the deployment:
STACK_NAME="my-sftpgw"
gcloud infra-manager deployments apply \
"projects/${SERVICE_PROJECT}/locations/${REGION}/deployments/${STACK_NAME}" \
--project="${SERVICE_PROJECT}" \
--local-source="." \
--inputs-file="terraform.tfvars" \
--service-account="projects/${SERVICE_PROJECT}/serviceAccounts/${IM_SA}"
Monitor status:
gcloud infra-manager deployments describe \
"projects/${SERVICE_PROJECT}/locations/${REGION}/deployments/${STACK_NAME}" \
--project="${SERVICE_PROJECT}"
Step 8: Retrieve the Static IP
gcloud compute addresses list \
--project="${SERVICE_PROJECT}" \
--filter="name~${STACK_NAME}" \
--format="value(address)"
Allow 8–12 minutes after the deployment reaches ACTIVE for instances to finish booting (Liquibase schema migration runs on first boot).
Cleanup
The shared VPC infrastructure in the host project (VPC, subnet, private service access) is not managed by the HA template and will not be deleted when the deployment is torn down.
To tear down the SFTP Gateway deployment:
- Remove Cloud SQL deletion protection:
gcloud sql instances patch "${STACK_NAME}-db" \ --no-deletion-protection \ --project="${SERVICE_PROJECT}" - Empty the GCS bucket:
gcloud storage rm -r "gs://YOUR_BUCKET_NAME/**" - Delete the Cloud SQL instance:
gcloud sql instances delete "${STACK_NAME}-db" --project="${SERVICE_PROJECT}" - Delete the Infrastructure Manager deployment (also removes the firewall rules from the host project):
gcloud infra-manager deployments delete \ "projects/${SERVICE_PROJECT}/locations/${REGION}/deployments/${STACK_NAME}" \ --project="${SERVICE_PROJECT}"
No VPC peering teardown steps are required — the private service access peering lives in the host project and was not created by this deployment.
Troubleshooting
Terraform fails at instance template with permission error
The service project has not been attached to the Shared VPC host. Confirm Step 2 was completed:
gcloud compute shared-vpc associated-projects list "${HOST_PROJECT}"
Terraform fails creating firewall rules
The IM service account is missing roles/compute.securityAdmin on the host project. Confirm Step 6 host project role grants are in place.
Cloud SQL fails to get a private IP
The private service access peering in the host project is not active, or the VPC name in existing_network doesn't match. Re-run the Step 5 validation commands.
Instances boot but can't reach Secret Manager or Cloud SQL APIs
Private Google Access is not enabled on the subnet. Confirm privateIpGoogleAccess: True in the Step 4 subnet validation output.