Cloud Run
GCP.Function runs an Effect program as a Cloud Run service. You
write a fetch handler, Alchemy builds a container image, pushes it
to Artifact Registry, and deploys a new revision. The service scales
with traffic and down to zero.
Declare a service
Section titled “Declare a service”import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export default class Api extends GCP.Function<Api>()( "Api", { main: import.meta.url }, Effect.gen(function* () { return { fetch: Effect.succeed(HttpServerResponse.text("Hello from Cloud Run!")), }; }),) {}GCP.Function is the same class as GCP.Run.Service. main names
the file to bundle.
Add it to the Stack
Section titled “Add it to the Stack”import * as Alchemy from "alchemy";import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import Api from "./src/Api.ts";
export default Alchemy.Stack( "MyApp", { providers: GCP.providers(), state: GCP.state() }, Effect.gen(function* () { const api = yield* Api; return { url: api.uri }; }),);api.uri is the service’s https://….run.app URL.
Make it public
Section titled “Make it public”A new service only accepts callers with a Google identity token and
roles/run.invoker. Set invokerIamDisabled to let anyone call it:
export default class Api extends GCP.Function<Api>()( "Api", { main: import.meta.url }, { main: import.meta.url, invokerIamDisabled: true },Leave it off for internal services. Another service calls a private
one with InvokeService.
Read the request
Section titled “Read the request”fetch is an Effect. Get the request from HttpServerRequest:
import { HttpServerRequest } from "effect/http/HttpServerRequest";
fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.originalUrl, "http://localhost");
if (request.method === "POST" && url.pathname === "/echo") { const body = yield* request.json; return yield* HttpServerResponse.json(body); }
return HttpServerResponse.text("Not found", { status: 404 });}).pipe(Effect.orDie),For typed routes, use Effect HTTP or
Effect RPC. Both produce a fetch handler.
Bind resources
Section titled “Bind resources”Declare a resource in the constructor and bind it. The binding returns a client and grants the service the role it needs on that resource:
Effect.gen(function* () { const links = yield* GCP.Firestore.Database("Links", { type: "FIRESTORE_NATIVE", }); const db = yield* GCP.Firestore.ReadWriteDatabase(links);
return { fetch: Effect.gen(function* () { const doc = yield* db.get("links/home"); return yield* HttpServerResponse.json(doc?.fields ?? {}); }).pipe(Effect.orDie), };}).pipe(Effect.provide(GCP.Firestore.ReadWriteDatabaseHttp)),Each service gets its own service account. Bindings grant roles to it and nothing else. See How bindings grant IAM.
Pass environment variables
Section titled “Pass environment variables”env sets plain container environment variables:
{ main: import.meta.url, env: { LOG_LEVEL: "debug", API_BASE: other.uri, },}Values can be Outputs from other resources. For credentials, use Secret Manager instead.
Choose the region
Section titled “Choose the region”A service deploys to the Stack’s region (your profile’s region, else
us-central1). Pin it with location:
{ main: import.meta.url, location: "europe-west1" }Changing location replaces the service.
Tune scaling
Section titled “Tune scaling”Keep warm instances to avoid cold starts, or cap the maximum:
{ main: import.meta.url, scaling: { minInstanceCount: 1, maxInstanceCount: 20 },}Size the container
Section titled “Size the container”Set CPU, memory, timeout, and concurrency on the revision template:
{ main: import.meta.url, template: { timeout: "60s", maxInstanceRequestConcurrency: 40, containers: [{ resources: { limits: { cpu: "2", memory: "1Gi" } } }], },}Every template change rolls out a new revision.
Receive events
Section titled “Receive events”Event sources make Google call your service. Each one creates the delivery (a push subscription, a scheduler job, an Eventarc trigger) and lets only that delivery in, so the service can stay private:
yield* GCP.PubSub.consumeTopicMessages(Orders, (messages) => messages.pipe(Stream.runForEach(({ message }) => handle(message))),);// …provided with Effect.provide(GCP.Run.TopicEventSource)See Pub/Sub, Cloud Scheduler, Storage events, and Eventarc.
Run without main
Section titled “Run without main”Leave out main to deploy any container image:
const hello = yield* GCP.Run.Service("Hello", { invokerIamDisabled: true, template: { containers: [{ image: "us-docker.pkg.dev/cloudrun/container/hello" }], },});Destroy
Section titled “Destroy”alchemy destroy deletes the service, its image repository, and the
service account Alchemy created for it, and revokes that account’s
roles.
Example
Section titled “Example”examples/gcp-cloud-run-api
is a link shortener on one service: Firestore for links, Secret
Manager for the API key.
Where next
Section titled “Where next”- Firestore — the datastore most services start with.
- Cloud Run jobs — work that runs to completion.
- Run reference — every prop and attribute.