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.
Instances and revisions
Section titled “Instances and revisions”Each deploy creates a revision, and traffic moves to it. A revision runs in instances: copies of your container that each take up to 80 requests at once by default. Cloud Run starts more instances as traffic grows and stops them when it falls, down to none.
You pay while instances serve requests, so an idle service costs nothing. Set scaling.minInstanceCount to keep some warm and skip the cold start.
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.
Cache build layers
Section titled “Cache build layers”Image builds reuse the local BuildKit cache of the machine they run on.
A fresh CI runner has none and rebuilds every step. Keep the layers in
Artifact Registry with build.cache:
{ main: import.meta.url, build: { cache: true },}Builds then import and export every stage’s layers at the repository’s
:buildcache tag. The cache is off by default and never changes the
image, so turning it on doesn’t roll out a revision. Other backends
take BuildKit’s cache backends, as in
Docker builds.
Check health
Section titled “Check health”Cloud Run’s default startup probe passes as soon as the port accepts a connection. Give it an HTTP probe so a revision only takes traffic once the program can serve:
{ main: import.meta.url, template: { containers: [ { startupProbe: { httpGet: { path: "/health" }, periodSeconds: 2, failureThreshold: 30 }, livenessProbe: { httpGet: { path: "/health" }, periodSeconds: 10 }, }, ], },}A failing liveness probe restarts the container. Probes can also use
tcpSocket or grpc. Remove startupProbe to go back to the default.
Add a sidecar
Section titled “Add a sidecar”Containers after the first run next to main and reach it on
localhost. Only the first container takes requests:
{ main: import.meta.url, template: { containers: [ { dependsOn: ["cache"] }, { name: "cache", image: "docker.io/library/redis:7-alpine", startupProbe: { tcpSocket: { port: 6379 } }, }, ], },}dependsOn starts main after cache passes its startup probe.
Sidecars covers start order, running your own
program as a sidecar, and binding it for a typed client.
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" }], },});An image service can list sidecars too; see Sidecars.
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.