Skip to content

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.

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.

instance 1requestrequestrequestrequestinstance 2requestrequesttraffic growsidle: no instances
requestcold start
Each instance serves many requests side by side. When traffic grows past what the running instances take, Cloud Run starts another, and when it stops, it stops them all.

You pay while instances serve requests, so an idle service costs nothing. Set scaling.minInstanceCount to keep some warm and skip the cold start.

src/Api.ts
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.

alchemy.run.ts
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.

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.

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.

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.

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.

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.

Keep warm instances to avoid cold starts, or cap the maximum:

{
main: import.meta.url,
scaling: { minInstanceCount: 1, maxInstanceCount: 20 },
}

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.

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.

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.

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.

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.

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.

alchemy destroy deletes the service, its image repository, and the service account Alchemy created for it, and revokes that account’s roles.

examples/gcp-cloud-run-api is a link shortener on one service: Firestore for links, Secret Manager for the API key.