Spawn and manage Google Compute Engine instances over the REST API.
Why REST?
The Compute Engine API is REST/JSON only — it has no gRPC endpoint.
This library talks to it with Req; there is no
connection pool or supervised process to run. A GcpCompute.Config is the
only handle you pass around.
Quickstart
# 1. Mint tokens with Goth (or any GcpCompute.TokenProvider)
children = [{Goth, name: MyApp.Goth}]
# 2. Build a config (do this once, 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 for it
{:ok, instance} =
GcpCompute.insert_instance_and_wait(config,
name: "worker-1",
machine_type: "e2-micro",
spot: true,
max_run_duration: 3600,
startup_script: "#!/bin/bash\necho ready > /tmp/ready"
)
GcpCompute.Instance.external_ip(instance)
#=> "34.x.x.x"
# 4. Tear it down
{:ok, _op} = GcpCompute.delete_instance_and_wait(config, "worker-1")The functions here are thin wrappers over GcpCompute.Instances and
GcpCompute.Operations; reach for those modules directly for the full surface.
Summary
Functions
Drop the cached config for {otp_app, key} so the next config/2 re-reads env.
Load (and cache) a config from application env.
See GcpCompute.Instances.serial_port_output/3 — the VM's serial console
output, which is where a :startup_script's output lands.
See GcpCompute.Instances.operations/3 — an instance's audit trail.
See GcpCompute.Instances.preemption/3 — the operation recording a Spot VM's
preemption, or nil. Works on an instance that is already gone.
Spawn an instance and wait until it's running.
See GcpCompute.Instances.simulate_maintenance_event/3 — on a Spot VM this
triggers a real preemption, for testing that your code survives one.
Delete an instance and wait for it to be gone.
Functions
Drop the cached config for {otp_app, key} so the next config/2 re-reads env.
Returns :ok whether or not an entry was cached. See config/2 for the
caching caveat (:persistent_term writes trigger a global GC).
@spec config(atom(), atom()) :: {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
Load (and cache) a config from application env.
# config :my_app, :gcp_compute, project: "p", zone: "...", token_provider: {...}
{:ok, config} = GcpCompute.config(:my_app)The result is memoized in :persistent_term on the first successful load and
reused on every later call. Caching is first-write-wins with no
invalidation: once cached, later changes to the application env are ignored.
Only successful loads are cached — an invalid env returns its {:error, _}
without caching, so it is re-read next call. To force a reload after
reconfiguring at runtime, call clear_config/2 first. (:persistent_term
writes trigger a global GC, so treat clearing as a rare operation, not a
per-request one.)
Cached configs are secrets at rest
The cached value is the whole %GcpCompute.Config{}, including its
:token_provider arg. With the production default
(GcpCompute.TokenProvider.Goth) that arg is just a registered process
name — nothing sensitive. With GcpCompute.TokenProvider.Static it is the
raw bearer token, and :persistent_term gives it none of the protection you
might assume:
- it is process-global — any code in the VM can read it with
:persistent_term.get/1, and:persistent_term.get/0enumerates everything; - it is unencrypted and written verbatim into
erl_crash.dump; - it is visible in
:observerand:recon; GcpCompute.Config'sInspectredaction does not apply — that only affectsinspect/1output, not term storage or a crash dump.
This is accepted, not fixed: skipping the cache for credential-bearing
providers would only move the token into the %Config{} that every caller
holds anyway, while quietly turning config/2 into an env re-read on every
call. For production, mint tokens with Goth (or your own provider) so no
long-lived credential is ever stored here.
@spec delete_instance(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
@spec delete_instance_and_wait(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
@spec from_env(atom(), atom()) :: {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
@spec get_instance(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}
@spec insert_instance(GcpCompute.Config.t(), GcpCompute.Instances.spec(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
@spec insert_instance_and_wait( GcpCompute.Config.t(), GcpCompute.Instances.spec(), keyword() ) :: {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}
@spec instance_logs(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, %{contents: binary(), start: integer(), next: integer()}} | {:error, GcpCompute.Error.t()}
See GcpCompute.Instances.serial_port_output/3 — the VM's serial console
output, which is where a :startup_script's output lands.
@spec instance_operations(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, [GcpCompute.Operation.t()]} | {:error, GcpCompute.Error.t()}
See GcpCompute.Instances.operations/3 — an instance's audit trail.
@spec instance_preempted?(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, boolean()} | {:error, GcpCompute.Error.t()}
@spec instance_preemption(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t() | nil} | {:error, GcpCompute.Error.t()}
See GcpCompute.Instances.preemption/3 — the operation recording a Spot VM's
preemption, or nil. Works on an instance that is already gone.
@spec launch(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Instance.t()} | {:error, GcpCompute.Error.t()}
Spawn an instance and wait until it's running.
Sugar over insert_instance_and_wait/3: the name is lifted out, and spec
options (machine_type, spot, …) are auto-separated from call options
(:zone, :request_id, :timeout, :poll_interval).
{:ok, vm} = GcpCompute.launch(config, "worker-1", machine_type: "e2-micro", spot: true)spot: true is the default
A bare launch(config, "worker-1") provisions a Spot VM that can be
preempted at any time and, when it is, deletes itself along with anything
stored on it (provisioningModel: "SPOT", automaticRestart: false,
instanceTerminationAction: "DELETE"). That is this library's intended
default — cheap, self-cleaning batch workers — but pass spot: false for
anything whose disk you care about.
@spec list_instances( GcpCompute.Config.t(), keyword() ) :: {:ok, [GcpCompute.Instance.t()]} | {:error, GcpCompute.Error.t()}
@spec list_instances_page( GcpCompute.Config.t(), keyword() ) :: {:ok, %{items: [GcpCompute.Instance.t()], next_page_token: String.t() | nil}} | {:error, GcpCompute.Error.t()}
@spec local(keyword()) :: {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
@spec new(keyword()) :: {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
@spec production(keyword()) :: {:ok, GcpCompute.Config.t()} | {:error, GcpCompute.Error.t()}
@spec simulate_maintenance_event(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
See GcpCompute.Instances.simulate_maintenance_event/3 — on a Spot VM this
triggers a real preemption, for testing that your code survives one.
@spec start_instance(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
@spec stop_instance(GcpCompute.Config.t(), String.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}
@spec terminate(GcpCompute.Config.t(), String.t(), keyword()) :: :ok | {:error, GcpCompute.Error.t()}
Delete an instance and wait for it to be gone.
Returns :ok — not {:ok, _} like every sibling — because the completed
delete operation carries nothing a caller needs. Errors still arrive as
{:error, %GcpCompute.Error{}}; use delete_instance_and_wait/3 if you want
the operation itself.
@spec wait_for_operation(GcpCompute.Config.t(), GcpCompute.Operation.t(), keyword()) :: {:ok, GcpCompute.Operation.t()} | {:error, GcpCompute.Error.t()}