How to connect services across VPCs privately using PSC, with Shared VPC and subnet design.

The Goal

Allow ArkCI dev runners (GitHub Actions self-hosted runners) to connect to Octopus servers (lab and prod) privately via internal networking β€” no public internet exposure.


Core Networking Concepts

VPC (Virtual Private Cloud)

A VPC is an isolated private network in Google Cloud. Resources in different VPCs cannot communicate by default.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      VPC-A          β”‚    βœ—     β”‚       VPC-B         β”‚
β”‚   10.0.0.0/16       │◄────────►│    10.1.0.0/16      β”‚
β”‚                     β”‚  Can't   β”‚                     β”‚
β”‚   Server A          β”‚  talk    β”‚    Server B         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Shared VPC

A Shared VPC allows multiple GCP projects to share a single VPC network. There’s one host project that owns the VPC, and multiple service projects that use it.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              SHARED VPC HOST: k-shared-vpc-host-dev                β”‚
β”‚                                                                         β”‚
β”‚   Network: shared-vpc-network-default                                   β”‚
β”‚                                                                         β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚   β”‚ Subnet:         β”‚  β”‚ Subnet:         β”‚  β”‚ Subnet:         β”‚       β”‚
β”‚   β”‚ citadel-dev-    β”‚  β”‚ github-actions- β”‚  β”‚ github-actions- β”‚       β”‚
β”‚   β”‚ tokyo           β”‚  β”‚ dev-virginia    β”‚  β”‚ dev-tokyo [NEW] β”‚       β”‚
β”‚   β”‚ 10.32.x.x/xx   β”‚  β”‚ 10.39.x.x/xx   β”‚  β”‚ 10.36.200.0/24  β”‚       β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚                      β”‚                      β”‚
          β–Ό                      β–Ό                      β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ SERVICE     β”‚      β”‚ SERVICE         β”‚    β”‚ SERVICE         β”‚
   β”‚ PROJECT:    β”‚      β”‚ PROJECT:        β”‚    β”‚ PROJECT:        β”‚
   β”‚ m-jp- β”‚      β”‚ k-github-  β”‚    β”‚ k-github-  β”‚
   β”‚ citadel-dev β”‚      β”‚ actions-dev     β”‚    β”‚ actions-dev     β”‚
   β”‚             β”‚      β”‚ (runners)       β”‚    β”‚ (PSC endpoints) β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Subnets

A subnet is a range of IP addresses within a VPC, tied to a specific region. Resources must be created in a subnet that exists in their target region.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        SHARED VPC NETWORK                               β”‚
β”‚                                                                         β”‚
β”‚    VIRGINIA (us-east4)              TOKYO (asia-northeast1)             β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”             β”‚
β”‚   β”‚ k-github-      β”‚          β”‚ k-github-      β”‚             β”‚
β”‚   β”‚ actions-dev-virginiaβ”‚          β”‚ actions-dev-tokyo   β”‚             β”‚
β”‚   β”‚                     β”‚          β”‚                     β”‚             β”‚
β”‚   β”‚ β€’ ArkCI runners     β”‚          β”‚ β€’ PSC endpoints     β”‚             β”‚
β”‚   β”‚   run here          β”‚          β”‚   (to reach Tokyo   β”‚             β”‚
β”‚   β”‚                     β”‚          β”‚    Octopus servers) β”‚             β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why two subnets?

  • ArkCI runners are in Virginia (closer to GitHub, better performance)
  • Octopus servers are in Tokyo (where most infrastructure lives)
  • PSC endpoints must be in the same region as the service they connect to

Private Service Connect (PSC)

PSC creates a private tunnel between VPCs without exposing services to the internet. It operates on a Consumer-Producer model over the Google Cloud backbone β€” the producer publishes a service via a Service Attachment, and the consumer creates a PSC Endpoint (an internal IP) in their own VPC that tunnels traffic privately to the producer.

What PSC Solves (vs VPC Peering)

Before PSC, connecting VPCs required VPC Peering, which has three major limitations PSC resolves:

  • IP Address Overlaps β€” PSC works even if consumer and producer use the same IP ranges (e.g., both use 10.0.0.0/24). Peering fails in this scenario.
  • Security Radius β€” PSC connects to a specific service, whereas peering opens connectivity to the entire network.
  • Operational Complexity β€” no need to coordinate IP ranges and firewall rules between teams.
FeaturePSCVPC Peering
Connectivity ScopeOne specific serviceEntire networks
IP OverlapAllowedNot allowed
Traffic FlowUnidirectional (Consumer β†’ Producer)Bidirectional
TransitivityAccessible from on-prem/peered networksNot transitive
AdministrationIndependent, no coordination neededRequires mutual agreement on IP ranges

Primary Use Cases

  • Google APIs β€” access BigQuery, Cloud Storage, Vertex AI via private internal IPs
  • SaaS consumption β€” connect to MongoDB Atlas, Snowflake, Confluent within Google’s network
  • Cross-organization access β€” share services across teams/acquisitions without merging networks or refactoring IP addresses

Components of PSC

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           SERVICE PROVIDER                              β”‚
β”‚                         (k-octopus-lab)                            β”‚
β”‚                                                                         β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚  Octopus    β”‚      β”‚  Internal Load  β”‚      β”‚  SERVICE         β”‚  β”‚
β”‚   β”‚  Pods       │─────►│  Balancer (ILB) │─────►│  ATTACHMENT      β”‚  β”‚
β”‚   β”‚             β”‚      β”‚                 β”‚      β”‚                  β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β”‚  "I'm publishing β”‚  β”‚
β”‚                                                  β”‚   this service"  β”‚  β”‚
β”‚                                                  β”‚                  β”‚  β”‚
β”‚                                                  β”‚  Allowed:        β”‚  β”‚
β”‚                                                  β”‚  β€’ citadel-dev   β”‚  β”‚
β”‚                                                  β”‚  β€’ citadel-lab   β”‚  β”‚
β”‚                                                  β”‚  β€’ github-actionsβ”‚  β”‚
β”‚                                                  β”‚    -dev [NEW]    β”‚  β”‚
β”‚                                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                            β”‚
                                              PSC Connectionβ”‚(private)
                                                            β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           SERVICE CONSUMER                β”‚             β”‚
β”‚                     (k-github-actions-dev)           β”‚             β”‚
β”‚                                                           β–Ό             β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚   β”‚                      PSC ENDPOINT                                β”‚  β”‚
β”‚   β”‚                                                                  β”‚  β”‚
β”‚   β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚  β”‚
β”‚   β”‚  β”‚  COMPUTE ADDRESS    β”‚      β”‚  FORWARDING RULE    β”‚           β”‚  β”‚
β”‚   β”‚  β”‚                     β”‚      β”‚                     β”‚           β”‚  β”‚
β”‚   β”‚  β”‚  Internal IP:       │◄────►│  Target: Service    β”‚           β”‚  β”‚
β”‚   β”‚  β”‚  10.36.200.x        β”‚      β”‚  Attachment URI     β”‚           β”‚  β”‚
β”‚   β”‚  β”‚                     β”‚      β”‚                     β”‚           β”‚  β”‚
β”‚   β”‚  β”‚  "Traffic to this   β”‚      β”‚  "Route traffic to  β”‚           β”‚  β”‚
β”‚   β”‚  β”‚   IP goes to PSC"   β”‚      β”‚   the provider"     β”‚           β”‚  β”‚
β”‚   β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚  β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

How PSC Works

  1. Provider creates a Service Attachment that wraps an Internal Load Balancer
  2. Provider whitelists allowed consumer projects (consumer_accept_lists)
  3. Consumer creates a PSC Endpoint (address + forwarding rule)
  4. The endpoint gets a private IP in the consumer’s VPC
  5. Traffic to that IP is tunneled to the provider’s service

DNS Configuration

For services to connect using a hostname instead of IP, we need DNS. See DNS Zones & Forwarding Rules for full details.

Private DNS Zone

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    PRIVATE DNS ZONE: psc.internal                       β”‚
β”‚                    (k-github-actions-dev)                          β”‚
β”‚                                                                         β”‚
β”‚   Visibility: Private (only visible to the dev shared VPC)              β”‚
β”‚                                                                         β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚   β”‚  DNS RECORDS                                                    β”‚   β”‚
β”‚   β”‚                                                                 β”‚   β”‚
β”‚   β”‚  octopus.lab.psc.internal  ──►  10.36.200.x (PSC endpoint IP)  β”‚   β”‚
β”‚   β”‚  octopus.prod.psc.internal ──►  10.36.200.y (PSC endpoint IP)  β”‚   β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Complete Data Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  ArkCI Dev Runner (Virginia)                                            β”‚
β”‚                                                                         β”‚
β”‚  curl https://octopus.lab.psc.internal/api                             β”‚
β”‚       β”‚                                                                 β”‚
β”‚       β”‚ Step 1: DNS Lookup                                              β”‚
β”‚       β–Ό                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚  β”‚  Private DNS Zone (psc.internal)            β”‚                       β”‚
β”‚  β”‚  octopus.lab.psc.internal β†’ 10.36.200.x     β”‚                       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚       β”‚                                                                 β”‚
β”‚       β”‚ Step 2: Connect to IP                                           β”‚
β”‚       β–Ό                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚  β”‚  PSC Endpoint (Tokyo subnet)                β”‚                       β”‚
β”‚  β”‚  10.36.200.x                                β”‚                       β”‚
β”‚  β”‚                                             β”‚                       β”‚
β”‚  β”‚  Forwarding Rule targets:                   β”‚                       β”‚
β”‚  β”‚  octopus-lab service attachment             β”‚                       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚       β”‚                                                                 β”‚
β”‚       β”‚ Step 3: PSC Tunnel (private, cross-VPC)                         β”‚
β”‚       β–Ό                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚
        β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Octopus Lab Server (Tokyo)                                             β”‚
β”‚                                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚  β”‚  Service Attachment                         β”‚                       β”‚
β”‚  β”‚  octopus-server-psc                         β”‚                       β”‚
β”‚  β”‚                                             β”‚                       β”‚
β”‚  β”‚  Accepts: k-github-actions-dev βœ“       β”‚                       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚       β”‚                                                                 β”‚
β”‚       β–Ό                                                                 β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                       β”‚
β”‚  β”‚  Internal Load Balancer β†’ Octopus Pods      β”‚                       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why Not Just Use Public URLs?

ApproachProsCons
Public URL (octopus.lab.citadelapps.com)Simple, no setupExposed to internet, requires firewall rules
PSC (octopus.lab.psc.internal)Private, secure, no internet exposureMore complex setup

PSC is preferred for internal services because:

  • Traffic never leaves Google’s network
  • No public IP exposure
  • Fine-grained access control via consumer_accept_lists

See also

Interview Prep

Q: What is Private Service Connect and how does it differ from VPC Peering?

A: PSC is a networking capability that allows VPCs to access specific services privately without peering entire networks. Unlike VPC Peering β€” which connects entire networks, requires non-overlapping IP ranges, and is bidirectional β€” PSC connects to a single service, allows overlapping IPs, and is unidirectional (consumer β†’ producer). PSC uses a Consumer-Producer model: the producer publishes a Service Attachment wrapping an Internal Load Balancer, and the consumer creates a PSC Endpoint (internal IP + forwarding rule) that tunnels traffic privately to that attachment.

Q: Can PSC work when consumer and producer VPCs have overlapping IP ranges?

A: Yes. This is one of PSC’s key advantages over VPC Peering. PSC creates a private tunnel at the SDN level β€” the consumer accesses the service via a local internal IP in their own VPC, and Google’s network fabric routes it to the producer’s service attachment. The two VPCs never merge routing tables, so overlapping CIDRs are not a problem.

Q: Why must a PSC endpoint be in the same region as the service it connects to?

A: PSC endpoints connect to Service Attachments, which are regional resources backed by regional Internal Load Balancers. The forwarding rule and compute address that form the PSC endpoint must be in the same region as the target service attachment. If your runners are in Virginia but the service is in Tokyo, you create the PSC endpoint in a Tokyo subnet β€” cross-region routing within the same VPC handles the rest.

Q: Walk through the complete data flow when a runner in Virginia connects to an Octopus server in Tokyo via PSC.

A: (1) The runner calls octopus.lab.psc.internal. (2) Cloud DNS resolves this via a private zone to the PSC endpoint’s internal IP (e.g., 10.36.200.x) in the Tokyo subnet. (3) The VPC routes the packet to Tokyo (same VPC, cross-region). (4) The forwarding rule on that IP matches and tunnels the packet to the producer’s Service Attachment URI. (5) The Service Attachment checks the consumer project against its consumer_accept_lists. (6) If allowed, traffic reaches the producer’s Internal Load Balancer, which forwards to the Octopus pods. The return path reverses through the same tunnel.

Q: What is the network field in a PSC forwarding rule, and why is it required even when the address already specifies a subnetwork?

A: Internal IPs like 10.x.x.x are only unique within a VPC. The network field tells the forwarding rule which VPC’s routing table to register in. In a Shared VPC, the network is managed by the host project while the forwarding rule lives in a service project β€” the explicit network field ensures the endpoint is visible to the entire shared VPC, not just the service project’s local scope.