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.

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.

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" }],
},
});

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.