A local GCP emulator for development and testing — Moto for Google Cloud
Project description
gcplocal
A local GCP emulator for development and testing — Moto for Google Cloud.
gcplocal runs a local HTTP server that emulates Google Cloud Platform APIs. Point Terraform, GCP client libraries, or any REST client at it and develop without touching real cloud infrastructure.
Supported Services
| Service | Resources | Terraform Resources |
|---|---|---|
| Compute Engine | Instances, Networks, Subnetworks, Firewalls, Disks | google_compute_instance, google_compute_network, google_compute_subnetwork, google_compute_firewall, google_compute_disk |
| Cloud Storage | Buckets, Objects, Bucket IAM | google_storage_bucket, google_storage_bucket_object |
| IAM | Service Accounts, Keys, Custom Roles | google_service_account, google_service_account_key, google_project_iam_custom_role |
| Pub/Sub | Topics, Subscriptions | google_pubsub_topic, google_pubsub_subscription |
| Cloud SQL | Instances, Databases, Users | google_sql_database_instance, google_sql_database, google_sql_user |
| Resource Manager | Projects, IAM Policies | google_project |
Installation
From PyPI (once published):
pip install gcplocal
From GitHub:
pip install git+https://github.com/gcplocal/gcplocal.git
From source:
git clone https://github.com/gcplocal/gcplocal.git
cd gcplocal
pip install -e .
Docker:
docker run -p 8400:8400 gcplocal/gcplocal
Or with docker-compose:
docker-compose up
Quick Start
Start the Emulator
gcplocal start
This starts the server on http://0.0.0.0:8400 with all services enabled.
Options:
gcplocal start --host 127.0.0.1 --port 9000 --debug
Use with Terraform
Generate the provider configuration:
gcplocal terraform-config
This outputs the HCL block you need. Or copy this into your main.tf:
provider "google" {
project = "gcplocal-project"
region = "us-central1"
access_token = "gcplocal-mock-token"
compute_custom_endpoint = "http://localhost:8400/compute/v1/"
storage_custom_endpoint = "http://localhost:8400/storage/v1/"
iam_custom_endpoint = "http://localhost:8400/v1/"
pubsub_custom_endpoint = "http://localhost:8400/v1/"
cloud_resource_manager_custom_endpoint = "http://localhost:8400/v1/"
sql_custom_endpoint = "http://localhost:8400/sql/v1beta4/"
}
Then run Terraform as usual:
terraform init
terraform plan
terraform apply -auto-approve
Use from Python
from gcplocal import create_app
app = create_app()
with app.test_client() as client:
# Create a Compute Engine instance
client.post(
"/compute/v1/projects/my-proj/zones/us-central1-a/instances",
json={"name": "test-vm", "machineType": "e2-micro"},
)
# Read it back
resp = client.get(
"/compute/v1/projects/my-proj/zones/us-central1-a/instances/test-vm"
)
print(resp.get_json()["status"]) # RUNNING
CLI Commands
| Command | Description |
|---|---|
gcplocal start |
Start the emulator server |
gcplocal terraform-config |
Generate Terraform provider config (HCL, JSON, or env vars) |
gcplocal status |
Check if the emulator is running |
gcplocal reset |
Clear all in-memory state |
Terraform Config Formats
gcplocal terraform-config --format hcl # Default: HCL provider block
gcplocal terraform-config --format json # JSON for programmatic use
gcplocal terraform-config --format env # Shell export statements
API Endpoints
All endpoints mirror the real GCP REST APIs:
GET /health # Health check
POST /reset # Clear all state
# Compute Engine
POST /compute/v1/projects/{p}/zones/{z}/instances # Create instance
GET /compute/v1/projects/{p}/zones/{z}/instances/{n} # Get instance
DELETE /compute/v1/projects/{p}/zones/{z}/instances/{n} # Delete instance
POST /compute/v1/projects/{p}/global/networks # Create network
POST /compute/v1/projects/{p}/global/firewalls # Create firewall
...
# Cloud Storage
POST /storage/v1/b # Create bucket
GET /storage/v1/b/{bucket} # Get bucket
POST /upload/storage/v1/b/{bucket}/o?name=... # Upload object
...
# IAM
POST /v1/projects/{p}/serviceAccounts # Create SA
GET /v1/projects/{p}/serviceAccounts/{email} # Get SA
...
# Pub/Sub
PUT /v1/projects/{p}/topics/{topic} # Create topic
PUT /v1/projects/{p}/subscriptions/{sub} # Create subscription
...
# Cloud SQL
POST /sql/v1beta4/projects/{p}/instances # Create SQL instance
POST /sql/v1beta4/projects/{p}/instances/{i}/databases # Create database
...
How It Works
gcplocal is a Flask application that:
- Registers service blueprints — each GCP service (Compute, Storage, IAM, etc.) is a separate module with routes matching the real GCP REST API
- Stores state in memory — a thread-safe
StateStoretracks all resources keyed by(service, project, resource_type, resource_id) - Returns realistic responses — JSON responses match the structure Terraform and GCP client libraries expect (selfLinks, operation objects, etags, etc.)
- Handles GCP Operations — Compute operations return immediately as
DONE, so Terraform doesn't wait for polling - Fakes authentication — all requests are accepted regardless of credentials
Architecture
gcplocal/
__init__.py # Package entry point
server.py # Flask app factory, middleware, service registration
state.py # Thread-safe in-memory resource store
auth.py # Mock authentication
cli.py # Click CLI (start, terraform-config, status, reset)
constants.py # API versions, defaults
utils.py # Shared helpers (IDs, timestamps, pagination, errors)
services/
base.py # Abstract service base class
compute/ # Compute Engine (instances, networks, firewalls, disks)
storage/ # Cloud Storage (buckets, objects)
iam/ # IAM (service accounts, keys, roles)
pubsub/ # Pub/Sub (topics, subscriptions)
resourcemanager/ # Resource Manager (projects)
sql/ # Cloud SQL (instances, databases, users)
terraform/
provider.py # Generates Terraform provider config files
Using in Your Test Suite
gcplocal ships as a pytest plugin. Install it in any project and the fixtures are available automatically:
# No imports needed — fixtures are auto-discovered
def test_create_bucket(gcplocal_client):
"""gcplocal_client is a Flask test client, no server needed."""
resp = gcplocal_client.post(
"/storage/v1/b?project=my-proj",
json={"name": "test-bucket", "location": "US"},
)
assert resp.status_code == 200
assert resp.get_json()["name"] == "test-bucket"
def test_with_real_http(gcplocal_url):
"""gcplocal_url starts a real HTTP server on a random port."""
import requests
resp = requests.get(f"{gcplocal_url}/health")
assert resp.json()["status"] == "healthy"
def test_terraform_endpoints(gcplocal_terraform_env):
"""Get a dict of all custom endpoints for Terraform config."""
env = gcplocal_terraform_env
assert "compute_custom_endpoint" in env
assert env["url"].startswith("http://")
Available Fixtures
| Fixture | Scope | Description |
|---|---|---|
gcplocal_client |
function | Flask test client (no HTTP server, fast) |
gcplocal_app |
function | Flask app instance for custom setups |
gcplocal_state |
function | Direct access to the state store (cleared per test) |
gcplocal_url |
session | Real HTTP server URL on a random port |
gcplocal_terraform_env |
function | Dict of all custom endpoints for Terraform |
Running Tests
pip install -e '.[dev]'
pytest tests/ -v
Publishing a Release
To PyPI
- Create a GitHub Release (tag like
v0.1.0) - The
publish.ymlworkflow automatically builds and uploads to PyPI via trusted publishing - Users can then
pip install gcplocal
Manual publish (if not using GitHub Actions):
pip install build twine
python -m build
twine upload dist/*
Docker Image
docker build -t gcplocal/gcplocal:latest .
docker push gcplocal/gcplocal:latest
Auto-Update System
gcplocal includes a code generator that scans Google's API Discovery Service and automatically generates emulator stubs for new or updated GCP services.
How it works
- Scanner (
tools/discovery.py) querieshttps://discovery.googleapis.com/discovery/v1/apisfor all GCP services and their REST API schemas - Generator (
tools/generator.py) takes the discovery document and produces a complete Flask service module with routes for every CRUD method - Registry (
gcplocal/service_registry.json) tracks auto-generated services, which the server loads dynamically at startup - GitHub Action (
.github/workflows/auto-update.yml) runs weekly, generates any missing services, and opens a PR automatically
Manual usage
# See what services are missing
python -m tools.update_services --scan-only
# Generate a specific service
python -m tools.update_services --service secretmanager
# Generate all missing services at once
python -m tools.update_services --all
Auto-generated services are registered in service_registry.json and loaded
automatically — no manual wiring needed.
Weekly auto-update
The auto-update.yml GitHub Action:
- Runs every Monday at 06:00 UTC
- Scans the Discovery API for new/changed services
- Generates stubs for anything missing
- Runs the test suite
- Opens a PR if there are changes
You can also trigger it manually from the Actions tab.
Adding New Services Manually
For hand-crafted (higher fidelity) service implementations:
- Create a new directory under
gcplocal/services/ - Subclass
GCPServiceand implementregister_routes() - Register it in
gcplocal/server.py→_register_services() - Add the endpoint constant in
constants.py
from gcplocal.services.base import GCPService
class MyNewService(GCPService):
service_name = "myservice"
api_prefix = "/myservice/v1"
def register_routes(self, bp):
bp.add_url_rule(
f"{self.api_prefix}/projects/<project>/things",
"list_things", self.list_things, methods=["GET"],
)
def list_things(self, project):
items = self.state.list("myservice", project, "things")
return jsonify({"items": items})
Hand-written services in server.py take priority over auto-generated ones
with the same name.
License
Apache 2.0
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gcplocal-0.1.0.tar.gz.
File metadata
- Download URL: gcplocal-0.1.0.tar.gz
- Upload date:
- Size: 39.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
915a7e7c16e17c92effa7c99796cf3a4e3fd93c28a761ccd8e7cfb8413728cc4
|
|
| MD5 |
ed7f6b3c992b792bdca1c9e0b569c91e
|
|
| BLAKE2b-256 |
0c24fde15b40fe7d7b10002bc6ab8bbadaf43b8f566b388aa65e1a7e042b8b10
|
Provenance
The following attestation bundles were made for gcplocal-0.1.0.tar.gz:
Publisher:
publish.yml on jjpf3/gcplocal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gcplocal-0.1.0.tar.gz -
Subject digest:
915a7e7c16e17c92effa7c99796cf3a4e3fd93c28a761ccd8e7cfb8413728cc4 - Sigstore transparency entry: 1115440746
- Sigstore integration time:
-
Permalink:
jjpf3/gcplocal@132425ad7af5d22be2cc48347b77e8b4bcdc2861 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jjpf3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@132425ad7af5d22be2cc48347b77e8b4bcdc2861 -
Trigger Event:
release
-
Statement type:
File details
Details for the file gcplocal-0.1.0-py3-none-any.whl.
File metadata
- Download URL: gcplocal-0.1.0-py3-none-any.whl
- Upload date:
- Size: 41.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e6d5133d33ad0ffea226d2d402ca4c0c94acad84c07a8cb7d6c70adefb770a5
|
|
| MD5 |
8484cfabaa333f88cf84cd5264b69a8f
|
|
| BLAKE2b-256 |
1f18cd9e8995029400e24c64680a9b27108f9cd88dd3f06a2daa01798460636e
|
Provenance
The following attestation bundles were made for gcplocal-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on jjpf3/gcplocal
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gcplocal-0.1.0-py3-none-any.whl -
Subject digest:
8e6d5133d33ad0ffea226d2d402ca4c0c94acad84c07a8cb7d6c70adefb770a5 - Sigstore transparency entry: 1115440748
- Sigstore integration time:
-
Permalink:
jjpf3/gcplocal@132425ad7af5d22be2cc48347b77e8b4bcdc2861 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/jjpf3
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@132425ad7af5d22be2cc48347b77e8b4bcdc2861 -
Trigger Event:
release
-
Statement type: