GcpCompute
Spawn and manage Google Compute Engine instances from Elixir, over the Compute REST API. Pluggable auth, telemetry on every call, and an ergonomic builder for the (verbose) instance body — with cheap, self-deleting Spot VMs as the default.
Why REST and not gRPC? The Compute Engine API is REST/JSON only — it has no gRPC endpoint (it's the one notable exception among GCP APIs). So unlike
pubsub_grpc, there is no connection pool to run: a request is a handful of calls dominated by the 20–40s the VM takes to boot, where pooling buys nothing.GcpComputetalks to it withReq; aGcpCompute.Configis the only handle you pass around. The right place for gRPC +grpc_connection_poolis an agent running inside the machine you spawn — see Roadmap.
Installation
def deps do
[
{:gcp_compute, "~> 0.1.0"},
# Recommended token provider (optional dependency):
{:goth, "~> 1.4"}
]
end
Quickstart
# 1. Start Goth to mint OAuth tokens (in your application supervision tree)
children = [{Goth, name: MyApp.Goth}]
# 2. Build a config once and reuse it
{:ok, config} =
GcpCompute.Config.production(
project: "my-project",
zone: "us-central1-a",
goth: MyApp.Goth
)
# 3. Spawn a cheap, self-deleting Spot VM and wait until it exists
{:ok, instance} =
GcpCompute.insert_instance_and_wait(config,
name: "worker-1",
machine_type: "e2-micro",
spot: true,
max_run_duration: 3600, # hard server-side TTL (seconds)
startup_script: "#!/bin/bash\necho ready > /tmp/ready",
labels: %{"owner" => "platform"}
)
GcpCompute.Instance.external_ip(instance) #=> "34.x.x.x"
# 4. Tear it down (and wait for the delete operation)
{:ok, _op} = GcpCompute.delete_instance_and_wait(config, "worker-1")
Prefer to drive the lifecycle yourself? Every mutating call returns an
GcpCompute.Operation you can poll:
{:ok, op} = GcpCompute.insert_instance(config, name: "worker-1", machine_type: "e2-micro")
{:ok, done} = GcpCompute.wait_for_operation(config, op, timeout: :timer.minutes(3))
Examples & guides
- Runnable examples in
examples/:stubbed_demo.exs— the full insert → poll → get flow offline, no GCP or credentials (elixir examples/stubbed_demo.exs).spawn_spot_vm.exs— a real (billable) Spot VM.
- Guides:Getting Started · Configuring Machines · Testing
Configuration
GcpCompute.Config is validated with NimbleOptions. Three builders cover the
common cases:
# Production — tokens minted by Goth
{:ok, config} = GcpCompute.Config.production(project: "p", goth: MyApp.Goth)
# From application env
# config :my_app, :gcp_compute,
# project: "p", zone: "europe-west4-a",
# token_provider: {GcpCompute.TokenProvider.Goth, MyApp.Goth}
{:ok, config} = GcpCompute.Config.from_env(:my_app, :gcp_compute)
# Local / emulator / tests — a static token, no Goth required
{:ok, config} = GcpCompute.Config.local(project: "p", base_url: "http://localhost:8080/compute/v1")
| Option | Default | Notes |
|---|---|---|
:project | — (required) | GCP project id. |
:zone | "us-central1-a" | Default zone; override per call with zone:. |
:token_provider | {TokenProvider.Goth, GcpCompute.Goth} | {module, arg} implementing GcpCompute.TokenProvider. |
:base_url | "https://compute.googleapis.com/compute/v1" | Point at the emulator or a proxy. |
:req_options | [] | Merged into every Req request (:retry, :adapter, …). |
Pluggable auth
Auth is a behaviour, GcpCompute.TokenProvider, so the library never hard-depends
on Goth. Built-ins: TokenProvider.Goth (production), TokenProvider.Static
(tests/emulator). Bring your own (workload identity, metadata server, Vault) by
implementing one callback:
defmodule MyApp.MetadataToken do
@behaviour GcpCompute.TokenProvider
@impl true
def fetch_token(_arg), do: {:ok, %{token: fetch_from_metadata_server()}}
end
Configurable machines
GcpCompute.Instance.spec/1 turns friendly options into the Compute insert body
(defaults shown):
| Option | Default |
|---|---|
:machine_type | "e2-micro" |
:source_image | debian-cloud/.../debian-12 |
:disk_size_gb | 10 |
:spot | true (SPOT, no auto-restart, DELETE on terminate) |
:max_run_duration | nil (set seconds for a hard server-side TTL) |
:external_ip | true |
:startup_script, :metadata, :labels, :tags, :network, :subnetwork, :service_account, :scopes | — |
Need a field it doesn't cover? Pass a raw Compute body map to
GcpCompute.Instances.insert/3 instead.
Telemetry
Every API call is a :telemetry.span/3:
| Event | Metadata |
|---|---|
[:gcp_compute, :request, :start] | method, path, project |
[:gcp_compute, :request, :stop] | + result (:ok/:error), http_status |
[:gcp_compute, :request, :exception] | + kind, reason, stacktrace |
GcpCompute.Telemetry.attach_default_logger() # dev convenience
# or wire into Telemetry.Metrics / your reporter
Testing without GCP
Tests stub the network via Req's native :adapter option — no Plug, no creds:
config =
GcpCompute.Config.local!(
project: "test",
req_options: [adapter: fn req -> {req, %Req.Response{status: 200, body: %{...}}} end]
)
See test/support/req_stub.ex for the routing-table helper used by the suite.
Roadmap
This package is the Compute client layer. A higher-level sandbox orchestration layer is planned on top of it:
gen_statemper sandbox (:provisioning → :probing → :running → :terminating)DynamicSupervisor+Registry, named profiles, per-user quotas- a reaper for TTL sweeps + orphan reconciliation (cloud resources outlive the BEAM)
- the gRPC hop: an in-VM agent reached via
grpc_connection_pool, lifecycle events published throughpubsub_grpc
License
MIT.